Transform your GitHub activity into professional social media content — powered by AI, designed for developers who code more than they post.
Most developers are active on GitHub but invisible on LinkedIn. Writing engaging posts takes time, and consistency is hard. This tool bridges the gap by:
- Scanning your GitHub activity to find meaningful coding moments
- Generating professional posts using AI (Groq LLM)
- Publishing directly to LinkedIn through official OAuth APIs
It's built for developers who want to grow their professional presence without spending hours crafting content.
Currently supports LinkedIn with GitLab, Bitbucket, Twitter/X, and Threads coming in future updates.
| Feature | Description |
|---|---|
| GitHub Activity Scanning | Fetches your public commits, PRs, pushes, and new repos |
| AI Post Generation | Uses Groq LLM to create natural, engaging LinkedIn content |
| OAuth-based LinkedIn Posting | Publishes via LinkedIn's official API with your authorization |
| Web Dashboard | Modern UI with dark/light mode, stats, and post management |
| Multi-User Support | Per-user credentials with Clerk authentication |
| Post Editor | Manual writing with character counter and preview |
| Image Integration | Optional Unsplash images for visual posts |
| CLI Bot Mode | Standalone script for scheduled, automated posting |
| Not Supported | Why |
|---|---|
| Scraping LinkedIn | Violates Terms of Service |
| Automated credential harvesting | Security and compliance risk |
| Mass/spam posting | Against LinkedIn's policies |
| Engagement automation (likes, comments) | Not supported; would violate ToS |
| Bypassing rate limits | Posts are made through official APIs only |
| Storing LinkedIn passwords | OAuth tokens only; never raw credentials |
- Dashboard: Real-time stats, post history, GitHub activity feed
- Bot Mode Panel: One-click workflow: Scan → Generate → Review → Publish
- Activity Filters: Filter by time range (1–30 days) and type (Push, PR, Commits, etc.)
- Free Tier Limits: Scanned activities capped at 10 to match daily post quota
- Commit Count Badges: Push events display commit counts (e.g., "3 commits")
- Settings Management: Individual save buttons per credential, masked secrets
- Dark/Light Mode: Full theme support across all pages
- Keyboard Shortcuts: Ctrl+Enter to publish, Escape to close modals
- Post Templates: Pre-built templates for common post types
- Character Counter: LinkedIn's 3000-character limit enforced
- No scheduled queue (yet) — posts are published immediately
- Single image per post — LinkedIn API limitation
- No LinkedIn analytics — engagement metrics not fetched from LinkedIn
- English-only AI — generated content is optimized for English
- No mobile app — web-only (responsive design available)
This project prioritizes safety and compliance:
| Aspect | Implementation |
|---|---|
| Authentication | OAuth 2.0 via LinkedIn's official flow |
| Token Storage | Encrypted, per-user token store (never shared) |
| API Compliance | Uses only sanctioned LinkedIn APIs (w_member_social) |
| No Automation Risk | User initiates all actions; no background scraping |
| Credential Masking | API keys displayed as gsk_xxxx...xxxx |
| CORS Protection | Backend only accepts authorized frontend origins |
| Clerk Integration | JWT-verified requests for multi-tenant security |
Note: This tool does NOT use browser automation, headless browsers, or any method that would violate LinkedIn's Terms of Service. All posting is done through LinkedIn's sanctioned Marketing/Share APIs.
This project implements a secure multi-tenant architecture with encryption at rest.
⚠️ Security Guarantee: Secrets NEVER reach the frontend. All API keys, tokens, and credentials are managed exclusively server-side.
Platform secrets that NEVER enter the database. Loaded from environment variables only.
| Secret | Purpose | Notes |
|---|---|---|
LINKEDIN_CLIENT_ID/SECRET |
OAuth app credentials | Operator-owned |
GROQ_API_KEY |
AI generation | Operator-owned |
GITHUB_TOKEN |
Higher API rate limits | Operator-owned |
UNSPLASH_ACCESS_KEY |
Image fetching | Operator-owned |
ENCRYPTION_KEY |
Token encryption key | 32-byte Fernet key |
CLERK_ISSUER |
JWT verification | Auth provider URL |
Per-user tokens stored in backend_tokens.db with Fernet encryption at rest.
| Data | Encrypted | Notes |
|---|---|---|
access_token |
✅ Yes | LinkedIn OAuth token |
refresh_token |
✅ Yes | Token refresh |
github_access_token |
✅ Yes | Optional GitHub PAT |
linkedin_user_urn |
No | User's LinkedIn ID |
github_username |
No | Public identifier |
scopes |
No | OAuth scopes granted |
User settings in user_settings.db contain preferences only, no secrets.
| Data | Type | Notes |
|---|---|---|
github_username |
Text | Public identifier |
preferences |
JSON | UI/UX preferences |
onboarding_complete |
Boolean | Setup status |
subscription_tier |
Text | free/pro/enterprise |
Every user's data is strictly isolated using Clerk user IDs as tenant keys:
┌─────────────────────────────────────────────────────────────┐
│ TENANT ISOLATION │
├─────────────────────────────────────────────────────────────┤
│ 1. Every DB query filters by user_id (Clerk ID) │
│ 2. get_token_by_user_id() is the ONLY retrieval method │
│ 3. Tokens encrypted with shared ENCRYPTION_KEY │
│ 4. User A cannot access User B's tokens/settings │
│ 5. Parameterized queries prevent SQL injection │
│ 6. Frontend receives connection STATUS only, never tokens │
└─────────────────────────────────────────────────────────────┘
Implementation Details:
services/token_store.py: All queries includeWHERE user_id=?services/user_settings.py: Tenant-scoped preference storage- No admin endpoints expose cross-tenant data
Algorithm: Fernet (AES-128-CBC + HMAC-SHA256)
# services/encryption.py
from cryptography.fernet import Fernet
ENCRYPTION_KEY = os.getenv('ENCRYPTION_KEY') # 32-byte URL-safe base64
fernet = Fernet(ENCRYPTION_KEY.encode())
# Encrypt: plaintext → "ENC:base64_ciphertext"
# Decrypt: "ENC:base64_ciphertext" → plaintextEnvironment Behavior:
| Environment | ENCRYPTION_KEY Present |
Behavior |
|---|---|---|
Production (ENV=production) |
✅ Yes | Encrypt/decrypt normally |
Production (ENV=production) |
❌ No | Fail fast with EncryptionKeyMissingError |
| Development | ✅ Yes | Encrypt/decrypt normally |
| Development | ❌ No |
Generate a key:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"Complete flow from storage to internal use:
┌─────────────────────────────────────────────────────────────────────┐
│ TOKEN LIFECYCLE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. STORE (OAuth callback) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ access_token = "abc123..." │ │
│ │ ↓ │ │
│ │ encrypt_value(access_token) │ │
│ │ ↓ │ │
│ │ "ENC:gAAAAABl..." (stored in SQLite) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 2. RETRIEVE (API request) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ get_token_by_user_id(user_id) │ │
│ │ ↓ │ │
│ │ "ENC:gAAAAABl..." (from SQLite) │ │
│ │ ↓ │ │
│ │ decrypt_value(encrypted_token) │ │
│ │ ↓ │ │
│ │ access_token = "abc123..." (in-memory only) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 3. USE (Internal only) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ linkedin_service.post(access_token) ← Server-side only │ │
│ │ github_service.fetch(github_pat) ← Server-side only │ │
│ │ │ │
│ │ ❌ NEVER returned to frontend │ │
│ │ ❌ NEVER logged in plaintext │ │
│ │ ❌ NEVER exposed in API responses │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 4. AUTO-MIGRATION (Legacy plaintext) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ If token doesn't start with "ENC:": │ │
│ │ 1. Encrypt in memory │ │
│ │ 2. Atomic UPDATE with optimistic locking │ │
│ │ 3. Set is_encrypted = 1 │ │
│ │ 4. Return decrypted value for current request │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
The app connects to GitHub in two modes:
1. Public Mode (Default)
| Aspect | Details |
|---|---|
| Requires | Only github_username |
| Endpoint | /users/{username}/events/public |
| Access | Public activity only (pushes, PRs, new repos) |
| Auth | App-level GITHUB_TOKEN for rate limit boost |
| Rate Limit | 5000 req/hr (with token) or 60 req/hr (without) |
| Privacy | No access to private repositories |
2. Authenticated Mode (Optional)
| Aspect | Details |
|---|---|
| Requires | User-provided Personal Access Token (PAT) |
| Endpoint | /users/{username}/events |
| Access | Private AND public activity |
| Auth | User's PAT (encrypted in token_store) |
| Rate Limit | 5000 req/hr |
| Privacy | Access to private repos for post generation |
Security Guarantees:
- ✅ GitHub PAT is encrypted at rest in
backend_tokens.db - ✅ PAT is never exposed to frontend components
- ✅ PAT is used only for fetching activity to generate posts
- ✅ Rate limit errors (403) are handled gracefully with fallback
Note: If no user PAT is provided, the app automatically uses Public Mode with the optional app-level
GITHUB_TOKENfor rate limit boosts.
The frontend NEVER receives sensitive data:
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND DATA FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ ALLOWED (Safe Data) │
│ ───────────────────── │
│ • linkedin_connected: boolean │
│ • github_connected: boolean │
│ • github_username: "public-user" │
│ • subscription_tier: "free" | "pro" │
│ • onboarding_complete: boolean │
│ │
│ ❌ NEVER SENT (Secrets) │
│ ───────────────────── │
│ • access_token │
│ • refresh_token │
│ • github_access_token (PAT) │
│ • groq_api_key │
│ • linkedin_client_secret │
│ • unsplash_access_key │
│ • Any masked versions (e.g., "gsk_xxxx...xxxx") │
│ │
└─────────────────────────────────────────────────────────────────────┘
API Endpoints:
| Endpoint | Returns |
|---|---|
GET /api/connection-status/{user_id} |
{linkedin_connected, github_connected, github_username} |
GET /api/settings/{user_id} |
{github_username, onboarding_complete, subscription_tier} |
POST /api/settings |
Accepts only: user_id, github_username, onboarding_complete |
No credential input fields exist in:
- ✅ Settings page
- ✅ Onboarding flow
- ✅ Dashboard
- ✅ Any frontend component
Test files are located in the tests/ directory:
tests/
├── test_github.py # GitHub API integration tests
└── verify_phase2_security.py # Security verification suite
# Run all Phase 2 security checks
py tests/verify_phase2_security.py
# Expected output: 6/6 tests passed| Test | Description |
|---|---|
encryption_production |
Verifies fail-fast when ENCRYPTION_KEY missing in production |
encryption_development |
Verifies plaintext fallback with warning in development |
token_migration |
Checks atomic transactions with commit/rollback |
github_auth |
Validates deterministic endpoint selection based on token |
frontend_secrets |
Confirms no secrets in API request models |
readme_docs |
Verifies documentation matches implementation |
| Layer | Technology |
|---|---|
| Frontend | Next.js 14, React 18, TypeScript, Tailwind CSS |
| Backend | Python 3.10+, FastAPI, Gunicorn + Uvicorn |
| Database | PostgreSQL (asyncpg) / SQLite (development) |
| Authentication | Clerk (frontend), JWT verification (backend) |
| AI | Groq LLM (llama3-70b-8192) |
| APIs | LinkedIn OAuth, GitHub REST API, Unsplash |
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT (Browser) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js @ :3000) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Pages │ │ Components │ │ Hooks │ │
│ │ Dashboard │ │ BotMode │ │ useAuth │ │
│ │ Settings │ │ PostEditor │ │ useTheme │ │
│ │ Onboarding │ │ Navbar │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ Clerk Auth (JWT) │
└─────────────────────────────────────────────────────────────────────┘
│
REST API calls
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ BACKEND (FastAPI @ :8000) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ backend/app.py │ │
│ │ /api/github/scan /api/posts /api/publish/full │ │
│ │ /api/settings /api/templates │ │
│ │ /auth/linkedin/* /api/contact │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ JWT Verification │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ SERVICES LAYER │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ ai_service │ │ linkedin_svc │ │ github_activity│ │
│ │ (Groq LLM) │ │ (OAuth+Post) │ │ (REST API) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ user_settings │ │ token_store │ │ image_service │ │
│ │ (SQLite) │ │ (SQLite) │ │ (Unsplash) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ EXTERNAL SERVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ LinkedIn │ │ GitHub │ │ Groq │ │ Unsplash │ │
│ │ (OAuth) │ │ (REST) │ │ (LLM) │ │ (Images) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
| Decision | Rationale |
|---|---|
| Frontend/Backend split | Clear separation allows independent deployment (Vercel + Railway) |
| Services layer | Business logic isolated from API routes; reusable by CLI bot |
| Per-user credentials | Multi-tenant by design; users bring their own API keys |
| PostgreSQL (prod) | Async with asyncpg for high concurrency; SQLite fallback for dev |
| Clerk for auth | Handles JWT, sessions, and user management out of the box |
| Gunicorn + Uvicorn | Production-grade ASGI server with multiple workers |
All services in services/ follow this pattern for multi-tenant support:
# services/example_service.py
def service_function(
required_params,
user_token: str = None, # User's credential (from token_store)
user_key: str = None # Per-user API key (from settings)
):
# 1. Try user-provided credential
credential = user_token or user_key
# 2. Fall back to app-level env var (for CLI/single-user mode)
if not credential:
credential = os.getenv('APP_LEVEL_KEY')
# 3. Fail gracefully if neither
if not credential:
logger.warning("No credential available")
return NoneService Summary:
| Service | File | User Context Params |
|---|---|---|
| GitHub Activity | github_activity.py |
token (PAT) |
| LinkedIn Posting | linkedin_service.py |
access_token, linkedin_user_urn |
| AI Generation | ai_service.py |
groq_api_key |
| Image Fetching | image_service.py |
unsplash_key |
Code Sharing:
┌─────────────────────────────────────────────────────────────────────┐
│ CODE SHARING MODEL │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CLI Bot (bot.py) Web App (backend/app.py) │
│ │ │ │
│ └─────────┬─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SHARED SERVICES (services/) │ │
│ │ github_activity • linkedin_service • ai_service • image │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ENV vars token_store.py │
│ (CLI mode) (Web mode) │
│ │
└─────────────────────────────────────────────────────────────────────┘
- Node.js 18+
- Python 3.10+
- Clerk account (clerk.com)
- LinkedIn Developer App (developers.linkedin.com)
- Groq API key (console.groq.com)
# Clone repository
git clone https://github.com/cliff-de-tech/linkedin-post-bot.git
cd linkedin-post-bot
# Backend setup
pip install -r requirements.txt
cp .env.example .env # Configure your API keys
# Frontend setup
cd web
npm install
cp .env.local.example .env.local # Configure Clerk keys# Terminal 1: Start backend
cd backend && python app.py
# Terminal 2: Start frontend
cd web && npm run devOpen http://localhost:3000 to access the dashboard.
New users go through a simple 4-step setup:
┌─────────────────────────────────────────────────────────────────┐
│ ONBOARDING FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Welcome │
│ ───────────────── │
│ Brief intro to PostBot, ~2 minute setup │
│ │
│ Step 2: GitHub Username │
│ ───────────────────────── │
│ Enter public GitHub username (e.g., "cliff-de-tech") │
│ Used to fetch public activity for post content │
│ │
│ [Optional] Connect GitHub OAuth │
│ ────────────────────────────── │
│ For private repository access │
│ Grants read-only access to your repos │
│ │
│ Step 3: Connect LinkedIn │
│ ──────────────────────── │
│ One-click OAuth connection │
│ User is redirected to LinkedIn, then back │
│ Token stored encrypted in backend │
│ │
│ Step 4: All Done │
│ ───────────────── │
│ Summary confirmation, redirect to dashboard │
│ │
└─────────────────────────────────────────────────────────────────┘
What's NOT collected:
- ❌ LinkedIn API secrets (server-managed)
- ❌ Groq API keys (server-managed)
- ❌ Unsplash keys (server-managed)
- ❌ Any technical credentials
The settings page (/settings) shows connection status only—no credential input:
Features:
- ✅ LinkedIn connection status (Connected / Not Connected)
- ✅ GitHub username display and edit
- ✅ GitHub OAuth status (for private repos)
- ✅ "Reconnect" / "Disconnect" for LinkedIn
- ✅ "Connect" / "Disconnect" for GitHub OAuth
- ✅ Token expiry date display
Not Displayed:
- ❌ Access tokens
- ❌ Refresh tokens
- ❌ API keys
- ❌ Any sensitive credentials
Backend Endpoints:
| Endpoint | Method | Description |
|---|---|---|
/api/connection-status/{user_id} |
GET | Returns boolean connection states only |
/api/disconnect-linkedin |
POST | Deletes stored LinkedIn OAuth token |
/api/disconnect-github |
POST | Deletes stored GitHub OAuth token |
PostBot ensures each user's data is strictly isolated and posts are generated from their own activity only.
┌─────────────────────────────────────────────────────────────────────┐
│ DATA FLOW ISOLATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ User A User B │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ GitHub A │ │ GitHub B │ │
│ │ username + │ │ username + │ │
│ │ token │ │ token │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SHARED INFRASTRUCTURE │ │
│ │ ┌─────────────────────────────────────────────────────────┐│ │
│ │ │ AI Service (App-level Groq key) ││ │
│ │ │ Receives: User-specific activity data ONLY ││ │
│ │ │ Never sees: Other users' data ││ │
│ │ └─────────────────────────────────────────────────────────┘│ │
│ │ ┌─────────────────────────────────────────────────────────┐│ │
│ │ │ Image Service (App-level Unsplash key) ││ │
│ │ │ Driven by: Post content context ││ │
│ │ │ No user secrets: Query based on post text only ││ │
│ │ └─────────────────────────────────────────────────────────┘│ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ LinkedIn A │ │ LinkedIn B │ │
│ │ OAuth token │ │ OAuth token │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
GitHub Activity:
| Scope | How It's Enforced |
|---|---|
| Username | Passed per-request, never shared |
| Token (if OAuth) | Retrieved from token_store by user_id |
| API Calls | Scoped to that user's repos/events only |
AI Generation:
| Aspect | Implementation |
|---|---|
| API Key | App-level GROQ_API_KEY (or user-provided) |
| Input Data | User-specific activity data only |
| Context | Never sees other users' activities |
Unsplash Images:
| Aspect | Implementation |
|---|---|
| API Key | App-level UNSPLASH_ACCESS_KEY |
| Query | Derived from post content keywords |
| User Secrets | Not used – purely content-driven |
Why This Matters:
- ✅ No cross-user data leakage – Each API call includes only that user's identifiers
- ✅ Posts are authentic – Generated from real user activity, not synthetic data
- ✅ Shared services are stateless – AI and image services don't retain user context
The FastAPI backend provides OpenAPI documentation at stable URLs:
| Endpoint | Description |
|---|---|
/openapi.json |
OpenAPI 3.0 specification (JSON) |
/docs |
Interactive Swagger UI documentation |
/redoc |
ReDoc alternative documentation |
Local development:
- OpenAPI spec: http://localhost:8000/openapi.json
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Production: The same endpoints are available at your deployed backend URL.
# Download OpenAPI spec for code generation
curl http://localhost:8000/openapi.json > openapi.json
# Or in production
curl https://your-backend.railway.app/openapi.json > openapi.jsonlinkedin-post-bot/
├── web/ # Next.js Frontend
│ ├── src/pages/ # Dashboard, Settings, Onboarding
│ ├── src/components/ # UI Components
│ └── src/hooks/ # Custom React hooks
├── backend/ # FastAPI Backend
│ ├── app.py # API server
│ └── middleware/ # Auth middleware
├── services/ # Core Business Logic
│ ├── ai_service.py # Groq AI integration
│ ├── github_activity.py # GitHub API client
│ ├── linkedin_service.py # LinkedIn posting
│ └── user_settings.py # Settings storage
├── bot.py # Standalone CLI bot
└── auth.py # OAuth helper
PostBot supports two modes of operation:
The web app (web/ + backend/) is designed for multi-user SaaS deployment:
| Aspect | Implementation |
|---|---|
| Authentication | Clerk (JWT-based) |
| Credentials | Per-user encrypted token storage |
| LinkedIn OAuth | User authorizes via web flow |
| GitHub Access | Username (public) + optional OAuth (private) |
| API Keys | App-level only (env vars) |
| Multi-User | ✅ Full tenant isolation |
Configuration:
# Backend .env
LINKEDIN_CLIENT_ID=... # Your LinkedIn app
LINKEDIN_CLIENT_SECRET=...
GROQ_API_KEY=... # Shared AI key
UNSPLASH_ACCESS_KEY=... # Shared image key
ENCRYPTION_KEY=... # Token encryption
# Frontend .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=...
CLERK_SECRET_KEY=...The standalone bot.py is designed for personal/scheduled use:
| Aspect | Implementation |
|---|---|
| Authentication | Environment variables only |
| Credentials | Single user, loaded from .env |
| LinkedIn OAuth | Pre-configured token (manual setup) |
| GitHub Access | Token from env var |
| API Keys | Personal keys in .env |
| Multi-User | ❌ Single user only |
Configuration:
# .env for CLI mode
LINKEDIN_ACCESS_TOKEN=... # Your personal token
LINKEDIN_USER_URN=... # Your LinkedIn URN
MY_GITHUB_USERNAME=...
GITHUB_TOKEN=... # Optional: higher rate limits
GROQ_API_KEY=...
UNSPLASH_ACCESS_KEY=...| Use Case | Recommended Mode |
|---|---|
| Personal automated posting | CLI (bot.py) |
| Scheduled cron jobs | CLI (bot.py) |
| Multi-user platform | Web App |
| Team/agency use | Web App |
| Quick one-off posts | Either |
Both modes use the same services/ modules:
CLI (bot.py) Web (backend/app.py)
│ │
└──────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ services/ │
│ - github_activity.py │
│ - ai_service.py │
│ - linkedin_service.py │
│ - image_service.py │
└───────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
ENV vars (CLI) token_store (Web)
- Scheduled posting queue (time-delayed publishing)
- Post drafts with save/restore
- Export post history to CSV
- Multi-account support (multiple LinkedIn profiles)
- AI persona customization (tone, style, hashtags)
- Docker deployment package
- Basic engagement analytics (post performance)
- Mobile companion app (React Native)
- Team/agency mode (manage multiple clients)
- Content calendar view
- LinkedIn analytics integration (if API permits)
Demo Coming Soon — check back for a live preview of the dashboard.
-
Connect Repository
- Go to vercel.com and import your GitHub repo
- Set root directory to
web
-
Configure Environment Variables
NEXT_PUBLIC_API_URL=https://your-backend-url.onrender.com NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxx CLERK_SECRET_KEY=sk_live_xxx NEXT_PUBLIC_REDIRECT_URI=https://your-frontend.vercel.app/auth/callback -
Deploy
- Vercel will auto-build with
npm run build - Production URL:
https://your-app.vercel.app
- Vercel will auto-build with
-
Create New Web Service
- Go to render.com and create a new Web Service
- Connect your GitHub repo
-
Configure Build
Root Directory: backend Build Command: pip install -r requirements.txt Start Command: gunicorn -w 4 -k uvicorn.workers.UvicornWorker app:app --bind 0.0.0.0:$PORT
-
Environment Variables
DATABASE_URL=postgresql://user:password@host:5432/dbname LINKEDIN_CLIENT_ID=your_linkedin_client_id LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret GROQ_API_KEY=gsk_your_groq_key ENCRYPTION_KEY=your_fernet_encryption_key CLERK_ISSUER=https://your-clerk-instance.clerk.accounts.dev -
Deploy
- Render auto-deploys on push to main
- Backend URL:
https://your-app.onrender.com
# Install Railway CLI
npm i -g @railway/cli
# Login and deploy
railway login
cd backend && railway up
cd ../web && railway upThe backend includes a production-ready Dockerfile:
# backend/Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user for security
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
# Production: Gunicorn with Uvicorn workers (4 workers)
CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "app:app", "--bind", "0.0.0.0:8000"]Build and run:
cd backend
docker build -t linkedin-post-bot-api .
docker run -p 8000:8000 \
-e DATABASE_URL=postgresql://... \
-e GROQ_API_KEY=gsk_... \
-e ENCRYPTION_KEY=... \
linkedin-post-bot-api# Run with hot-reload
cd backend && uvicorn app:app --reload --port 8000
cd web && npm run dev
# Run tests
cd backend && pytest tests/ -v
cd web && npm test
# Lint & type check
cd web && npm run lint && npm run build| Issue | Solution |
|---|---|
| "No GitHub activity found" | Verify GITHUB_USERNAME, extend day range |
| LinkedIn auth fails | Check Client ID/Secret and redirect URL |
| AI posts are empty | Verify GROQ_API_KEY is valid |
| "Invalid token" | Re-authenticate with LinkedIn |
| Credentials not saving | Restart backend, check console logs |
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Commit changes:
git commit -m 'Add your feature' - Push to branch:
git push origin feature/your-feature - Open a Pull Request
MIT License — free for personal and commercial use.
Built by cliff-de-tech | Happy posting! 🚀