LinkAI is a dual-sided LinkedIn automation SaaS that brings together recruiter outreach workflow building and applicant auto-apply functionality under one roof. It is built around AI-driven personalization, anti-detection architecture, and a safety-first philosophy that keeps a human in the loop at every critical step.
The goal is simple: help recruiters and job seekers manage their LinkedIn activity at scale without getting flagged, banned, or lost in manual busywork. Everything from connection requests to follow-up messages can be orchestrated through a visual workflow builder, while the underlying engine handles timing, fingerprinting, and warm-up so the user does not have to think about it.
For the practical local setup and first-run flow, see the usage guide.
- Why LinkAI Exists
- How It Works (High Level)
- Architecture Overview
- Tech Stack
- Project Structure
- Core Features
- Chrome Extension -- Session Bridge
- Authentication and Multi-Tenancy
- LinkedIn Account Lifecycle
- Session Encryption
- 21-Day Warmup Engine
- Anti-Detection and Human Behavior Simulation
- Browser Fingerprint Manager
- Lead Management and CRM
- Smart Inbox
- Campaigns and Sequences
- Visual Workflow Builder
- Message Templates and Personalization
- Analytics Dashboard
- Engagement and Enrichment
- Landing Page
- Data Flow Diagrams
- Database Schema
- API Reference
- Getting Started
- Environment Variables
- Roadmap
- License
LinkedIn outreach is time-consuming, repetitive, and easy to get wrong. Send too many connection requests in a day and LinkedIn restricts your account. Use generic messages and prospects ignore you. Manage multiple accounts or team members and things fall apart fast.
LinkAI was built to solve these problems at their root. Instead of bolting automation on top of a browser and hoping for the best, it takes a different approach: extract session cookies from the user's real browser through a lightweight Chrome extension, run all orchestration server-side with proper anti-detection measures, and gradually warm up each account over 21 days so LinkedIn never sees a sudden spike in activity.
The platform serves two audiences. Recruiters get a full outreach CRM with campaign sequences, template personalization, and team collaboration. Job seekers (in a planned phase) will get AI-powered job matching and automated Easy Apply workflows. Both sides share the same underlying engine for safe, human-like LinkedIn interaction.
The typical user flow looks like this:
- A user signs up through the web app and installs the Chrome extension.
- The extension extracts LinkedIn session cookies (li_at and JSESSIONID) from the user's browser and sends them to the backend.
- The backend encrypts and stores these cookies, then begins a 21-day warmup period for the account.
- During warmup, daily action limits scale gradually using a sigmoid curve, starting at roughly 5 percent of full capacity and reaching 100 percent by day 22.
- The user builds outreach workflows using a visual drag-and-drop builder, sets up message templates with dynamic variables, and imports leads via CSV or LinkedIn URLs.
- The campaign engine executes sequences step by step: view a profile, wait a day, send a connection request, check if it was accepted, then send a follow-up message or fall back to email.
- All interactions are made to look human -- mouse movements follow Bezier curves, typing simulates realistic speed with occasional typos, and actions only happen during business hours in the lead's timezone.
- Results flow into the analytics dashboard, where users can track acceptance rates, reply rates, and campaign performance over time.
LinkAI uses a hybrid architecture. A lightweight Chrome extension handles authentication by extracting cookies from the user's real browser session. The FastAPI backend handles everything else -- orchestration, AI personalization, encryption, and (in future phases) server-side browser automation via Playwright with residential proxies.
+------------------------------------------------------------------------+
| USER'S BROWSER |
| +------------------+ +------------------------------------------+ |
| | Chrome Extension | | React Frontend (SPA) | |
| | - li_at cookie | | - Landing Page (particles, parallax) | |
| | - JSESSIONID |--->| - Dashboard, Inbox, Leads, Campaigns | |
| | - User-Agent | | - Workflow Builder (React Flow) | |
| +--------+---------+ | - Analytics (Recharts) | |
| | | - Templates, Sequences, Settings | |
| | +-------------------+----------------------+ |
+-----------+-----------------------------+-----------------------------+
| POST /api/linkedin/connect | REST API + JWT Auth
v v
+------------------------------------------------------------------------+
| FastAPI Backend (Python) |
| +------------+ +----------------+ +-----------------------------+ |
| | API Routes | | Services | | Middleware | |
| | - auth | | - linkedin | | - TenantMiddleware (RLS) | |
| | - linkedin | | - encryption | | - CORS (frontend + ext) | |
| | - leads | | - warmup | +-----------------------------+ |
| | - inbox | | - human_behavior| |
| | - templates| | - browser_mgr | +-----------------------------+ |
| | - campaigns| | - analytics | | Models (SQLAlchemy) | |
| | - sequences| | - sequence_eng | | - User, Tenant, Invitation | |
| | - analytics| | - inbox | | - LinkedInAccount | |
| | - engage | | - enrichment | | - Lead, Template, Sequence | |
| | - enrichment| | - email | | - Conversation, ConnReq | |
| | - anti_det | | - proxy | +-----------------------------+ |
| +------------+ +----------------+ |
+------------------------+----------------------------------------------+
|
+-------------+-------------+
v v v
+-----------+ +---------+ +-------------------+
| PostgreSQL| | Redis | | LinkedIn (Ext.) |
| 16 + pgv | | 7-alpine| | - Voyager API |
| - RLS | | - Cache | | - Authwall |
| - Alembic | | - Queue | | - Session cookies |
+-----------+ +---------+ +-------------------+
graph TB
subgraph Browser ["User's Browser"]
EXT["Chrome Extension<br/>Cookie Extractor"]
FE["React Frontend<br/>TypeScript + Vite"]
end
subgraph Backend ["FastAPI Backend"]
API["API Layer<br/>14 Route Modules"]
SVC["Service Layer<br/>17 Services"]
MW["Middleware<br/>Tenant Isolation"]
MDL["Models<br/>SQLAlchemy ORM"]
end
subgraph Infra ["Infrastructure"]
PG[("PostgreSQL 16<br/>+ pgvector")]
RD[("Redis 7<br/>Cache + Queue")]
end
subgraph External ["External"]
LI["LinkedIn API<br/>Voyager / Profile"]
PROXY["Residential<br/>Proxies"]
end
EXT -->|"li_at + JSESSIONID"| API
FE -->|"REST + JWT"| API
API --> SVC
API --> MW
SVC --> MDL
MDL --> PG
SVC --> RD
SVC -->|"Validate Session"| LI
SVC -.->|"Future: Playwright"| PROXY
PROXY -.-> LI
The project is split across three layers -- frontend, backend, and infrastructure -- with each tool chosen for a specific reason.
| Layer | Technology | Version | Why It Is Used |
|---|---|---|---|
| Frontend | React + TypeScript | 19.2 + 5.9 | Type-safe single-page application with component reuse |
| Build Tool | Vite | 7.3 | Fast hot module replacement and ESM-native builds |
| Routing | React Router | 7.13 | Client-side navigation without full page reloads |
| Workflow UI | React Flow (xyflow) | 12.10 | Visual drag-and-drop workflow builder, used by Stripe and Typeform |
| Charts | Recharts | 3.8 | Composable chart components for the analytics dashboard |
| Icons | Lucide React | 0.577 | Consistent, lightweight icon set across the interface |
| HTTP Client | Axios | 1.13 | API communication with JWT interceptor for token refresh |
| Backend | FastAPI + Uvicorn | 0.115 | Async Python API with automatic OpenAPI documentation |
| ORM | SQLAlchemy (async) | 2.0.35 | Database models and queries with full async support |
| Migrations | Alembic | 1.13 | Versioned schema migrations that track every database change |
| Database | PostgreSQL + pgvector | 16 | Relational storage with vector search for future AI features |
| Cache | Redis | 7 | Session caching, rate limiting, and background job queues |
| Auth | JWT (python-jose) | -- | Stateless access tokens (15-min) and refresh tokens (30-day) |
| Encryption | AES-256-GCM (cryptography) | 43.0 | Authenticated encryption for LinkedIn session cookies |
| Validation | Pydantic | 2.9 | Request and response schema validation with type coercion |
| HTTP Client (BE) | httpx | 0.27 | Async HTTP client for server-to-server LinkedIn API calls |
| Extension | Chrome MV3 | -- | Minimal cookie extraction bridge with no content scripts |
| Container | Docker Compose | 3.9 | One-command local development infrastructure |
LinkAI/
|-- docker-compose.yml # PostgreSQL 16 + pgvector, Redis 7
|-- PRD.md # Product Requirements Document
|-- roadmap.md # 22 Epics, 79 User Stories, 3 Phases
|-- README.md
|
|-- backend/
| |-- .env # Environment configuration
| |-- requirements.txt # Python dependencies
| |-- alembic.ini # Migration configuration
| |-- alembic/
| | |-- env.py # Alembic async engine setup
| | +-- versions/ # 5 migration files
| |
| +-- app/
| |-- main.py # FastAPI app factory + middleware registration
| |-- config.py # Pydantic Settings (reads from .env)
| |-- database.py # Async SQLAlchemy engine and session maker
| |-- dependencies.py # Dependency injection: get_current_user, get_tenant_id
| |
| |-- api/ # 14 route modules
| | |-- auth.py # signup, login, refresh, password reset
| | |-- users.py # user profile CRUD
| | |-- team.py # invitations and role management
| | |-- linkedin.py # connect, health check, proxy, limits
| | |-- lead.py # lead CRUD, CSV import, tagging, search
| | |-- inbox.py # conversations, messages, labels
| | |-- templates.py # message templates with variables and A/B testing
| | |-- analytics.py # dashboard metrics and time-series data
| | |-- connection_requests.py # batch scheduling and approval
| | |-- sequences.py # multi-step sequences with state tracking
| | |-- engagement.py # profile views, endorsements, likes
| | |-- enrichment.py # email finder and data enrichment
| | +-- anti_detection.py # fingerprint, proxy, warmup status
| |
| |-- models/ # 10 SQLAlchemy models
| | |-- tenant.py # multi-tenant organization
| | |-- user.py # users with tenant_id and roles
| | |-- invitation.py # team invitations
| | |-- linkedin_account.py # encrypted sessions, proxy config, limits
| | |-- lead.py # prospect profiles with tags
| | |-- conversation.py # LinkedIn message threads
| | |-- connection_request.py # outbound connection tracking
| | |-- sequence.py # outreach sequence definitions
| | +-- template.py # message templates with variants
| |
| |-- schemas/ # 11 Pydantic request/response DTOs
| | |-- auth.py, user.py, team.py, linkedin.py, lead.py
| | |-- inbox.py, template.py, sequence.py
| | |-- analytics.py, connection_request.py
| | +-- (all with Config.from_attributes = True)
| |
| |-- services/ # 17 business logic modules
| | |-- auth_service.py # JWT creation/verification, password hashing
| | |-- email_service.py # transactional email delivery
| | |-- linkedin_service.py # session validation and health checks
| | |-- encryption_service.py # AES-256-GCM encrypt and decrypt
| | |-- warmup_service.py # 21-day sigmoid curve limit calculation
| | |-- browser_manager.py # isolated Playwright browser contexts
| | |-- human_behavior.py # Bezier mouse, typing simulation, scroll
| | |-- proxy_service.py # residential proxy rotation
| | |-- analytics_service.py
| | |-- inbox_service.py
| | |-- template_service.py
| | |-- sequence_engine.py
| | |-- action_scheduler.py
| | |-- connection_request_service.py
| | |-- engagement_service.py
| | +-- enrichment_service.py
| |
| +-- middleware/
| +-- tenant.py # extracts tenant_id from JWT on every request
|
|-- frontend/
| |-- package.json # React 19, Vite 7, React Flow, Recharts
| |-- vite.config.ts
| |-- tsconfig.json
| |-- index.html
| |
| +-- src/
| |-- main.tsx # React entry point
| |-- App.tsx # Route definitions
| |-- index.css # Global theme with CSS custom properties
| |
| |-- api/
| | +-- client.ts # Axios instance with JWT interceptor
| |
| |-- contexts/
| | +-- AuthContext.tsx # Auth state management, login/logout, token refresh
| |
| |-- components/
| | |-- Logo.tsx + Logo.css
| | |-- ProtectedRoute.tsx
| | +-- ui/
| | |-- Button.tsx + .css
| | |-- Card.tsx + .css
| | +-- Input.tsx + .css
| |
| |-- layouts/
| | |-- AuthLayout.tsx + .css # Login and signup page wrapper
| | +-- DashboardLayout.tsx + .css # Sidebar navigation and top bar
| |
| +-- pages/ # 16 page components
| |-- LandingPage.tsx + .css # Animated landing with particles and parallax
| |-- Login.tsx, Signup.tsx # Auth forms
| |-- ForgotPassword.tsx, ResetPassword.tsx
| |-- Dashboard.tsx + .css # Main metrics overview
| |-- LinkedInAccounts.tsx + .css # Account connection and management
| |-- Leads.tsx + .css # Lead table with filtering and tagging
| |-- Inbox.tsx + .css # Threaded conversation view
| |-- Templates.tsx + .css # Template editor with live preview
| |-- Campaigns.tsx + .css # Campaign management
| |-- Sequences.tsx + .css # Sequence status tracking
| |-- WorkflowBuilder.tsx + .css # React Flow visual editor
| |-- Analytics.tsx + .css # Charts and time-series metrics
| |-- Engagement.tsx + .css # Profile views and endorsements
| |-- Enrichment.tsx + .css # Email finder tool
| |-- TeamSettings.tsx + .css # Team management and invitations
| +-- Profile.tsx + .css # User profile settings
|
+-- extension/
|-- manifest.json # Chrome Manifest V3
|-- background.js # Service worker for cookie extraction
|-- popup.html # Extension popup interface
|-- popup.js # Connect flow logic
|-- popup.css # Popup styling
+-- icons/ # 16, 48, 128px PNG + SVG icons
The Chrome extension is deliberately minimal. It does exactly one thing: extract LinkedIn session cookies from the user's logged-in browser and send them to the backend. There are no content scripts, no DOM injection, and no page modification.
This design matters because LinkedIn's detection systems look for extensions that manipulate page content. By keeping the extension limited to cookie reading, the risk profile drops significantly.
sequenceDiagram
participant U as User
participant EXT as Chrome Extension
participant BG as Background Script
participant LI as LinkedIn (cookies)
participant API as FastAPI Backend
participant DB as PostgreSQL
U->>EXT: Click "Connect LinkedIn"
EXT->>BG: sendMessage("extractCookies")
BG->>LI: chrome.cookies.get("li_at")
BG->>LI: chrome.cookies.get("JSESSIONID")
LI-->>BG: cookie values
BG->>BG: Read navigator.userAgent
BG-->>EXT: {li_at, jsessionid, user_agent}
EXT->>API: POST /api/linkedin/connect<br/>(Bearer JWT + cookies)
API->>API: AES-256-GCM encrypt session
API->>DB: INSERT linkedin_accounts<br/>(status=WARMING_UP)
API->>API: Best-effort profile fetch<br/>(may fail from server IP)
API-->>EXT: 201 Created {account}
EXT-->>U: "Account Connected"
A few things worth noting about this flow:
- The extension only reads cookies. It never writes to the page or injects scripts.
- Server-side validation of the LinkedIn session is best-effort. If the backend's IP gets blocked by LinkedIn (which is common), the connection still succeeds because the cookies themselves are known to be valid -- they just came from the user's browser.
- There is a 24-hour grace period after connection. Health checks during this window will not mark an account as expired, even if LinkedIn blocks the server-side validation attempt.
The platform supports teams and organizations out of the box through a multi-tenant architecture.
- JWT Access Tokens have a 15-minute TTL and carry the user's ID, tenant ID, and role.
- Refresh Tokens are stored as HttpOnly cookies with a 30-day TTL and rotate on each use.
- Tenant Middleware runs on every request, extracting the tenant ID from the JWT and scoping all database queries to that tenant.
- PostgreSQL Row-Level Security ensures that even if a query accidentally omits the tenant filter, data isolation is maintained at the database level.
- Team Invitations allow admins to bring in team members with specific roles (admin or member).
graph LR
A["Login Request"] --> B{"Validate Credentials"}
B -->|"Success"| C["Generate JWT<br/>access_token + refresh_token"]
C --> D["Set HttpOnly Cookie<br/>refresh_token"]
C --> E["Return access_token<br/>in JSON body"]
B -->|"Failure"| F["401 Unauthorized"]
G["API Request"] --> H{"Extract JWT"}
H --> I["get_current_user<br/>dependency"]
I --> J["Extract tenant_id"]
J --> K["Scope all DB queries<br/>to tenant"]
Each connected LinkedIn account is treated as a first-class entity with its own state machine. Accounts move between statuses based on real health check results and user actions.
stateDiagram-v2
[*] --> WARMING_UP: Extension connects
WARMING_UP --> ACTIVE: 21 days complete
WARMING_UP --> EXPIRED: Token truly invalid<br/>(401 after grace period)
ACTIVE --> EXPIRED: Token expired (401/403)
ACTIVE --> RESTRICTED: LinkedIn restricted (999)
EXPIRED --> WARMING_UP: Reconnect via extension
RESTRICTED --> WARMING_UP: Re-auth + reset
ACTIVE --> DISCONNECTED: User disconnects
WARMING_UP --> DISCONNECTED: User disconnects
Available account management endpoints:
| Method | Endpoint | What It Does |
|---|---|---|
| POST | /api/linkedin/connect |
Connect a new account via Chrome extension cookies |
| GET | /api/linkedin/accounts |
List all connected accounts for the current tenant |
| GET | /api/linkedin/accounts/{id} |
Detailed view including warmup progress |
| POST | /api/linkedin/accounts/{id}/check-health |
Manually trigger a session health check |
| PATCH | /api/linkedin/accounts/{id}/proxy |
Attach or update a residential proxy |
| PATCH | /api/linkedin/accounts/{id}/limits |
Adjust daily action limits |
| DELETE | /api/linkedin/accounts/{id} |
Disconnect and clean up the browser profile |
LinkedIn session cookies are sensitive credentials. If someone gains access to a user's li_at cookie, they can impersonate that user on LinkedIn. LinkAI encrypts these cookies at rest using AES-256-GCM, which provides both confidentiality and integrity.
graph LR
subgraph Encrypt
A["Session Data<br/>li_at + JSESSIONID + UA"] -->|"json.dumps"| B["Plaintext Bytes"]
B -->|"AES-256-GCM"| C["Ciphertext + Auth Tag"]
C -->|"base64"| D["encrypted_session"]
E["Random 96-bit"] -->|"base64"| F["encryption_nonce"]
end
subgraph Decrypt
D -->|"base64 decode"| G["Ciphertext"]
F -->|"base64 decode"| H["Nonce"]
G -->|"AES-256-GCM"| I["Plaintext JSON"]
I -->|"json.loads"| J["Session Dict"]
end
- Algorithm: AES-256-GCM (authenticated encryption with associated data).
- Key Derivation: A base64-decoded 32-byte key. If the configured key is shorter, it is hashed using SHA-256 to produce a 32-byte key.
- Nonce: A unique 96-bit random value is generated for each encryption operation and never reused.
- Storage: Both the ciphertext and nonce are stored as base64-encoded strings in PostgreSQL.
Brand-new LinkedIn accounts (or accounts that have been dormant) tend to get flagged if they suddenly start sending dozens of connection requests. The warmup engine addresses this by gradually increasing daily action limits over a 21-day period using a sigmoid curve.
Scaling Factor vs. Warmup Day
1.0 | *---------*
| *
0.8 | *
| *
0.6 | *
| *
0.4 | *
| *
0.2 | *
| *
0.05 |*
+--+--+--+--+--+--+--+--+--+--+--+--
1 2 4 6 8 10 12 14 16 18 20 22
Warmup Day
The formula is factor = 1 / (1 + e^(-0.35 * (day - 10))), which produces a smooth S-curve centered around day 10.
In practice, the daily limits scale like this:
| Day | Scaling | Connections/Day | Messages/Day |
|---|---|---|---|
| 1 | ~5% | 1 | 2 |
| 7 | ~25% | 6 | 12 |
| 10 | ~50% | 12 | 25 |
| 14 | ~75% | 18 | 37 |
| 21 | ~97% | 24 | 48 |
| 22+ | 100% | 25 | 50 |
Full daily limits for a mature account (100 percent scaling):
| Action | Daily Maximum |
|---|---|
| Connection Requests | 25 |
| Messages | 50 |
| Profile Views | 75 |
| Easy Apply | 35 |
| Endorsements | 15 |
| Likes | 20 |
LinkedIn uses machine learning to detect automated activity. Bots that click at the center of elements, type at a constant speed, or send requests at perfectly regular intervals are easy to catch. The human_behavior.py service generates realistic interaction parameters for every LinkedIn action.
graph TB
subgraph Mouse ["Bezier Mouse Movement"]
M1["Start Point"] --> M2["Control Point 1<br/>random offset"]
M2 --> M3["Control Point 2<br/>random offset"]
M3 --> M4["End Point"]
M4 -.-> M5["15% chance:<br/>Overshoot + Correct"]
end
subgraph Typing ["Typing Simulation"]
T1["40-60 WPM base speed"]
T2["Pause after punctuation"]
T3["2% thinking hesitation"]
T4["1.5% typo + backspace"]
end
subgraph Delays ["Action Delays (Gaussian)"]
D1["Minor: mean=5s, std=1.5s"]
D2["Major: mean=75s, std=25s"]
D3["Profile View: mean=180s, std=60s"]
D4["Think Pause: mean=35s, std=15s"]
end
subgraph Scroll ["Scroll Behavior"]
S1["Variable chunks 100-400px"]
S2["30% reading pauses 1-4s"]
S3["8% scroll-back"]
S4["Inertia deceleration"]
end
The key techniques:
- Mouse paths follow cubic Bezier curves with Fitts's Law velocity profiling. The cursor moves slowly near the start and end points and faster in the middle, with random micro-jitter of plus or minus 2.5 pixels per step. Fifteen percent of movements overshoot the target and self-correct.
- Click targeting avoids element centers. Click positions use a Gaussian offset within the element bounds, and the duration between mousedown and mouseup varies from 50 to 200 milliseconds.
- Typing is simulated character by character at 40 to 60 words per minute with natural pauses after punctuation. Two percent of characters trigger a "thinking hesitation" and 1.5 percent produce a typo followed immediately by a backspace correction.
- Delays between actions follow Gaussian distributions rather than uniform random. LinkedIn's detection systems can identify uniformly distributed delays because they look unnatural.
- Business hours constraint ensures all actions happen between 8 AM and 6 PM in the lead's timezone, which matches how a real person would use LinkedIn.
Every LinkedIn account gets a deterministic, consistent browser fingerprint derived from its account UUID. The fingerprint never changes between sessions for the same account, which is critical because LinkedIn cross-references fingerprint attributes and flags inconsistencies.
graph LR
A["Account UUID"] -->|"SHA-256"| B["Seed Integer"]
B --> C["Viewport<br/>1366x768 to 1920x1080"]
B --> D["WebGL Renderer<br/>Intel / NVIDIA / AMD"]
B --> E["Navigator<br/>Platform, RAM, Cores"]
B --> F["Canvas Noise Seed<br/>Imperceptible pixel noise"]
B --> G["Audio Context Offset<br/>Tiny float perturbation"]
B --> H["Screen Config<br/>Resolution, Color Depth"]
B --> I["Font Set<br/>Subset of common fonts"]
B --> J["Locale + Timezone"]
The following properties are spoofed to create a unique but realistic browser identity:
- Canvas (pixel-level noise invisible to the human eye)
- WebGL (renderer string and vendor)
- Navigator (platform, hardwareConcurrency, deviceMemory, maxTouchPoints, languages)
- Screen (resolution, colorDepth, pixelRatio)
- Audio Context (tiny floating-point offset)
- Font enumeration (a consistent subset of system fonts)
The lead management system is built for teams that work with large prospect lists.
- CSV import lets users upload lead lists from external tools or from LinkedIn Sales Navigator exports. LinkedIn URL list ingestion is also supported.
- Tagging and filtering by status, industry, location, or any custom tag. Full-text search works across name, company, and tags.
- Lead detail panel shows the full interaction history for a given prospect -- when they were contacted, what was sent, whether they replied, and which campaign they belong to.
- CSV export for moving data into external CRM tools.
The inbox provides a unified view of all LinkedIn conversations across connected accounts.
- All conversations from every connected LinkedIn account appear in a single interface, so users do not need to switch between accounts.
- Users can reply to messages directly from the inbox without opening LinkedIn.
- Messages are automatically labeled: new, replied, or follow-up needed.
- LinkedIn is polled every 5 to 15 minutes per account to keep conversations current.
Campaigns in LinkAI are not just bulk message blasts. They are multi-step sequences with conditional branching.
- A typical sequence might look like: send connection request, wait for acceptance, send a personalized message, wait three days, check for a reply, and send a follow-up if none came.
- Conditional logic lets the sequence branch based on outcomes. If a connection request times out after seven days, the system can fall back to an email outreach instead.
- Auto-pause on reply ensures that prospects who respond are immediately removed from the automated sequence so a human can take over the conversation.
- Per-prospect tracking shows exactly where each lead is in the sequence and what step comes next.
The workflow builder is powered by React Flow (xyflow), the same library used by Stripe, Typeform, and OneSignal for their visual editors.
graph TD
START["Find Leads"] --> VIEW["View Profile"]
VIEW --> WAIT1["Wait 1 Day"]
WAIT1 --> CONNECT["Send Connection"]
CONNECT --> CHECK{"Accepted?"}
CHECK -->|"Yes"| MSG["Send Message"]
CHECK -->|"No, 7d timeout"| EMAIL["Email Fallback"]
MSG --> WAIT2["Wait 3 Days"]
WAIT2 --> CHECK2{"Replied?"}
CHECK2 -->|"Yes"| DONE["Mark as Interested"]
CHECK2 -->|"No"| FU["Send Follow-Up"]
FU --> END["End Sequence"]
EMAIL --> END
style START fill:#22c55e20,stroke:#22c55e
style CONNECT fill:#8b5cf620,stroke:#8b5cf6
style CHECK fill:#f59e0b20,stroke:#f59e0b
style MSG fill:#3b82f620,stroke:#3b82f6
style EMAIL fill:#ef444420,stroke:#ef4444
Key capabilities:
- Drag-and-drop node types: Action, Condition, Delay, and Trigger nodes can be placed and connected freely.
- Serialization: Workflows are stored as JSON (nodes + edges + configuration) in PostgreSQL, making them portable and versionable.
- Auto-layout with ELK.js for complex branching workflows is planned for an upcoming release.
Templates support dynamic variable substitution so the same template produces different output for each lead.
- Variables like
{{first_name}},{{company}},{{position}}, and{{industry}}are replaced with actual lead data at send time. - Live preview lets users see exactly what a message will look like for a specific lead before sending.
- A/B testing tracks which variant of a template gets better response rates, so users can iterate on their messaging.
- AI-generated icebreakers are planned for Phase 3 and will use the lead's recent activity to craft a personalized opening line.
The analytics dashboard gives users a clear picture of how their outreach is performing.
- Key metrics: connections sent, acceptance rate, reply rate, and InMail performance are displayed as summary cards.
- Time-series charts built with Recharts show trends over time, with daily and weekly aggregation options.
- Filtering by campaign, date range, or lead segment lets users drill into specific subsets of their data.
- Real-time updates via WebSocket are planned for future releases.
Before sending a connection request or message, it helps to warm up the relationship with lighter touches. The engagement tools automate these pre-outreach interactions.
- Profile viewing with configurable daily limits acts as a soft introduction. The prospect sees your name in their "Who viewed your profile" list.
- Skill endorsements automatically endorse the top skills listed on a prospect's profile.
- Post likes engage with a prospect's recent content before making direct contact.
- Email enrichment uses GDPR-compliant third-party services to find prospect email addresses for multi-channel outreach.
- Multi-channel workflows combine LinkedIn and email touchpoints in a single sequence.
The landing page is built entirely in React and CSS with no external animation libraries. It is designed to communicate the product's value quickly through interactive elements.
- Canvas particle field with 60 interconnected particles and dynamic connection lines that respond to mouse movement.
- Typewriter effect cycles through key use cases: "Outreach", "Recruiting", "Lead Gen", "Pipeline".
- Parallax mouse tracking on floating decoration cards that follow the cursor.
- Scroll-triggered animations using IntersectionObserver with staggered reveal timing.
- Animated counters that count up to target values as they scroll into view.
- Interactive dashboard mockup with tabbed preview of the actual product (Dashboard, Inbox, Campaigns, Analytics tabs).
- Visual workflow diagram showing an animated outreach sequence.
- Pricing toggle between monthly and annual plans with an animated switch.
- FAQ accordion with smooth expand/collapse transitions.
This diagram shows what happens from the moment a user clicks "Connect LinkedIn" in the extension through to the account being stored in the database.
sequenceDiagram
participant Ext as Chrome Extension
participant API as FastAPI
participant Enc as Encryption Service
participant Val as LinkedIn Validator
participant DB as PostgreSQL
Ext->>API: POST /api/linkedin/connect<br/>{li_at, jsessionid, user_agent}
Note over API: Token is FRESH from browser<br/>Guaranteed valid
API->>Val: validate_linkedin_session()<br/>(best-effort, may fail)
Val->>Val: GET linkedin.com/voyager/api/me<br/>(from SERVER IP)
alt LinkedIn allows (200)
Val-->>API: {valid: true, name, profile_url}
else LinkedIn blocks (302/429/timeout)
Val-->>API: {valid: false, reason: "server_blocked"}
Note over API: Failure is OK --<br/>token is still valid
end
API->>Enc: encrypt_session(cookies)
Enc-->>API: (ciphertext, nonce)
API->>DB: INSERT account<br/>status=WARMING_UP
API-->>Ext: 201 Created
Health checks are designed to avoid false positives. During the first 24 hours after connection, failed validation from the server IP does not change the account status.
flowchart TD
A["Health Check Triggered"] --> B{"Account age < 24h?"}
B -->|"Yes"| C{"Age < 5 min?"}
C -->|"Yes"| D["Skip check entirely<br/>Return current status"]
C -->|"No"| E["Run validation"]
B -->|"No"| E
E --> F["Decrypt session"]
F --> G["Call LinkedIn API<br/>from server IP"]
G -->|"200 OK"| H["Update profile info<br/>Preserve WARMING_UP"]
G -->|"401/403"| I{"Within 24h grace?"}
I -->|"Yes"| J["Treat as server_blocked<br/>Do not change status"]
I -->|"No"| K["Set status = EXPIRED"]
G -->|"302 redirect"| L["Classify as server_blocked<br/>Do not change status"]
G -->|"429/timeout"| L
style D fill:#22c55e20,stroke:#22c55e
style J fill:#f59e0b20,stroke:#f59e0b
style K fill:#ef444420,stroke:#ef4444
style L fill:#3b82f620,stroke:#3b82f6
The schema is designed around multi-tenancy. Every table includes a tenant_id foreign key that ties records to a specific organization. PostgreSQL row-level security policies enforce isolation at the database level.
erDiagram
TENANTS ||--o{ USERS : has
TENANTS ||--o{ LINKEDIN_ACCOUNTS : has
USERS ||--o{ LINKEDIN_ACCOUNTS : owns
TENANTS ||--o{ LEADS : has
TENANTS ||--o{ TEMPLATES : has
TENANTS ||--o{ SEQUENCES : has
LINKEDIN_ACCOUNTS ||--o{ CONVERSATIONS : has
LINKEDIN_ACCOUNTS ||--o{ CONNECTION_REQUESTS : sends
TENANTS {
uuid id PK
string name
datetime created_at
}
USERS {
uuid id PK
uuid tenant_id FK
string email
string hashed_password
string full_name
string role
datetime created_at
}
LINKEDIN_ACCOUNTS {
uuid id PK
uuid tenant_id FK
uuid user_id FK
string linkedin_name
string linkedin_profile_url
text encrypted_session
string encryption_nonce
jsonb proxy_config
jsonb daily_limits
enum status
datetime warmup_started_at
datetime last_health_check
jsonb fingerprint_seed
}
LEADS {
uuid id PK
uuid tenant_id FK
string name
string company
string title
jsonb tags
string status
}
TEMPLATES {
uuid id PK
uuid tenant_id FK
string name
text body
jsonb variables
}
SEQUENCES {
uuid id PK
uuid tenant_id FK
string name
jsonb workflow_definition
string status
}
The backend exposes a RESTful API organized by domain. All endpoints (except auth and health) require a valid JWT access token.
| Module | Method | Endpoint | Description |
|---|---|---|---|
| Auth | POST | /api/auth/signup |
Register a new user and create a tenant |
| POST | /api/auth/login |
Authenticate and receive JWT tokens | |
| POST | /api/auth/refresh |
Exchange a refresh token for a new access token | |
| POST | /api/auth/forgot-password |
Request a password reset email | |
| POST | /api/auth/reset-password |
Reset password using a token from email | |
| GET | /api/health |
Service health check (no auth required) | |
| POST | /api/linkedin/connect |
Connect an account via extension cookies | |
| GET | /api/linkedin/accounts |
List all connected accounts | |
| GET | /api/linkedin/accounts/{id} |
Get detailed account info and warmup progress | |
| POST | /api/linkedin/accounts/{id}/check-health |
Trigger a manual session health check | |
| PATCH | /api/linkedin/accounts/{id}/proxy |
Configure a residential proxy for the account | |
| PATCH | /api/linkedin/accounts/{id}/limits |
Adjust daily action limits | |
| DELETE | /api/linkedin/accounts/{id} |
Disconnect and remove an account | |
| Leads | GET | /api/leads |
List leads with filtering and search |
| POST | /api/leads |
Create a new lead or import from CSV | |
| GET | /api/leads/{id} |
Get lead details and interaction history | |
| PATCH | /api/leads/{id} |
Update lead information or tags | |
| DELETE | /api/leads/{id} |
Remove a lead | |
| Inbox | GET | /api/inbox/conversations |
List all conversations across accounts |
| GET | /api/inbox/conversations/{id} |
Get messages in a conversation thread | |
| Templates | GET | /api/templates |
List message templates |
| POST | /api/templates |
Create a new template | |
| GET | /api/templates/{id} |
Get template details including variants | |
| PATCH | /api/templates/{id} |
Update a template | |
| DELETE | /api/templates/{id} |
Remove a template | |
| Campaigns | GET | /api/campaigns |
List campaigns |
| POST | /api/campaigns |
Create a new campaign | |
| Sequences | GET | /api/sequences |
List outreach sequences |
| POST | /api/sequences |
Create a new sequence | |
| Analytics | GET | /api/analytics/dashboard |
Get summary dashboard metrics |
| GET | /api/analytics/time-series |
Get time-series data for charts | |
| Engagement | POST | /api/engagement/view-profile |
Trigger a profile view action |
| Enrichment | POST | /api/enrichment/find-email |
Look up a prospect's email address |
Interactive API documentation is available when the backend is running:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
You will need the following installed on your machine:
- Docker and Docker Compose -- for running PostgreSQL and Redis locally.
- Python 3.11 or later -- for the FastAPI backend.
- Node.js 18 or later and npm -- for the React frontend.
- Google Chrome -- for loading the extension.
From the project root, bring up the database and cache services:
docker-compose up -dVerify that both containers are running:
docker-compose ps
# linkai-db -> postgres:5432
# linkai-redis -> redis:6379This starts PostgreSQL 16 with the pgvector extension on port 5432 and Redis 7 on port 6379.
cd backend
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows
# Install Python dependencies
pip install -r requirements.txt
# Make sure your .env file is in place (see Environment Variables below)
# Run database migrations
alembic upgrade head
# Start the API server
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Once running, the API is available at:
- API root: http://localhost:8000
- Swagger docs: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
cd frontend
# Install Node dependencies
npm install
# Start the development server
npm run devThe frontend will be available at http://localhost:5173. From there you can access the landing page, create an account, and explore the full dashboard.
- Open Google Chrome and navigate to
chrome://extensions. - Enable Developer Mode using the toggle in the top-right corner.
- Click Load unpacked.
- Select the
extension/folder from the project root. - The LinkAI extension icon will appear in the Chrome toolbar.
- Sign up at http://localhost:5173/signup and log in.
- Navigate to the LinkedIn Accounts page in the dashboard.
- Click Connect Account and follow the on-screen instructions:
- Copy the access token shown in the dashboard.
- Open the LinkAI Chrome extension by clicking its icon in the toolbar.
- Paste the token into the extension and click Save.
- Make sure you are logged into LinkedIn in Chrome.
- Click Connect LinkedIn in the extension popup.
- Your account will appear with a WARMING_UP status.
- Over the next 21 days, daily action limits will gradually increase according to the sigmoid warmup curve described above.
Create a .env file in the backend/ directory with the following variables:
# Database
DATABASE_URL=postgresql+asyncpg://linkai:linkai_dev_2026@localhost:5432/linkai
# Redis
REDIS_URL=redis://localhost:6379/0
# Authentication
SECRET_KEY=your-secret-key-change-in-production
ACCESS_TOKEN_EXPIRE_MINUTES=15
REFRESH_TOKEN_EXPIRE_DAYS=30
ALGORITHM=HS256
# Session Encryption (AES-256-GCM)
# Generate a key with: python -c "from app.services.encryption_service import generate_encryption_key; print(generate_encryption_key())"
ENCRYPTION_KEY=your-base64-encoded-32-byte-key
# Frontend URL (used for CORS and email links)
FRONTEND_URL=http://localhost:5173Make sure to replace placeholder values with real secrets before deploying to any shared or production environment.
The project is organized into three phases spanning 22 epics and 79 user stories.
Phase 1 (MVP) ======================== Weeks 1-12
Auth, LinkedIn Connect, Leads, Inbox,
Templates, Analytics, Integrations
Phase 2 (Automation) ............================ Weeks 10-20
Scheduled Sends, Sequence Builder,
Workflow Builder, Engagement, Enrichment,
Anti-Detection Hardening
Phase 3 (Applicant) ............................======== Weeks 18-30
AI Job Matching, Easy Apply, Knowledge Base,
AI Messages, Drip Campaigns, GDPR, Scale Infra
Current Status: Phase 1 is complete. Phase 2 is in progress, with Epics 9 through 14 implemented. The sequence engine, workflow builder, engagement tools, and anti-detection hardening are functional. Phase 3 work on the applicant-side features has not yet started.
email: demo@linkai.com password: Demo1234!
Proprietary. All rights reserved.