A self-hosted analytics and publishing dashboard for YouTube and Instagram.
Your data. Your infrastructure. No subscriptions.
Dashboard overview tracking YouTube and Instagram accounts side by side
Every analytics product wants a monthly subscription and access to your data. Twiligent is the alternative: a self-hosted dashboard that pulls your YouTube and Instagram stats directly from their official APIs, displays them in one place, and runs entirely on infrastructure you control.
You deploy one Cloudflare Worker and one Cloudflare Pages site using your own API credentials. Your data is stored in your own Supabase database and cached in your own Upstash Redis instance. No shared infrastructure. No accounts created on third-party services beyond the free tiers you choose. No ongoing cost.
- Features
- Screenshots
- Quick Start
- Prerequisites
- Installation
- Configuration
- Scheduled Publishing with GitHub Actions
- How It Works
- Project Structure
- Tech Stack
- Contributing
- License
| Feature | Details |
|---|---|
| Multi-account support | Add unlimited YouTube channels and Instagram Business or Creator accounts |
| YouTube analytics | Views, subscribers, likes, comments, engagement rate, avg views per video, Shorts vs. long-form breakdown, top videos, Tags and SEO analysis |
| Instagram analytics | Followers, engagement rate, virality score, consistency score, best posting day and hour, content-type performance, hashtag analysis, caption-length correlation |
| Unified overview | Cross-platform totals, audience comparison chart, audience share pie, leaderboards |
| Content explorers | Sortable and filterable grids for all YouTube Videos, all YouTube Shorts, and all Instagram Reels and posts |
| Scheduled publishing | Schedule Instagram photos, Reels, and Stories; a Cloudflare Worker cron publishes them every 15 minutes automatically |
| Bulk upload | Upload multiple files at once, assign captions individually or apply a shared caption, schedule them with configurable intervals |
| Cloudinary CDN | Upload media directly from the dashboard; Instagram requires public URLs before publishing |
| Token auto-refresh | Instagram long-lived tokens renew automatically every 24 hours via a daily Worker cron so analytics never break |
| Authentication | Supabase Auth with email and password; each user's accounts and posts are fully isolated |
| Dark and light mode | Toggle between themes from the header |
Clone and install dependencies:
git clone https://github.com/spacesdrive/twiligent.git
cd twiligent/backend && npm install
cd ../frontend && npm installCreate backend/.dev.vars with your credentials (see Configuration), then start both servers:
# Terminal 1 - backend (port 8787)
cd backend && wrangler dev
# Terminal 2 - frontend (port 5173)
cd frontend && npm run devOpen http://localhost:5173 and sign up for an account, then go to Accounts to add your first YouTube channel or Instagram account.
| Requirement | Notes |
|---|---|
| Node.js 20 | Required for Wrangler CLI |
| Wrangler CLI | npm install -g wrangler, then wrangler login |
| Cloudflare account | Free tier sufficient for Workers and Pages |
| Supabase project | Free tier sufficient; create at supabase.com |
| YouTube Data API v3 key | Required for YouTube analytics |
| Instagram Graph API | Meta app of type Business required |
| Cloudinary account | Required for content publishing only |
1. Clone the repository
git clone https://github.com/spacesdrive/twiligent.git
cd twiligent2. Install dependencies
cd backend && npm install
cd ../frontend && npm install3. Create the Supabase database tables
In your Supabase project, open the SQL editor and run the following:
-- Accounts table
create table accounts (
id text primary key,
user_id uuid not null references auth.users(id) on delete cascade,
platform text not null,
data jsonb not null
);
-- Settings table
create table settings (
user_id uuid not null references auth.users(id) on delete cascade,
key text not null,
value jsonb,
primary key (user_id, key)
);
-- Scheduled posts table
create table scheduled_posts (
id text primary key,
user_id uuid not null references auth.users(id) on delete cascade,
account_id text not null references accounts(id) on delete cascade,
status text not null default 'pending',
scheduled_at timestamptz not null,
data jsonb
);4. Configure credentials
See Configuration below.
5. Start the development servers
# Terminal 1
cd backend && wrangler dev
# Terminal 2
cd frontend && npm run devThe backend runs on port 8787. The frontend runs on port 5173. Open http://localhost:5173.
Create backend/.dev.vars (this file is gitignored):
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
YOUTUBE_API_KEY=AIzaSy...
INSTAGRAM_APP_ID=123456789
INSTAGRAM_APP_SECRET=abc123...
CLOUDINARY_CLOUD_NAME=mycloud
CLOUDINARY_UPLOAD_PRESET=ml_default
BACKEND_URL=http://localhost:8787
FRONTEND_URL=http://localhost:5173
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...
UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are optional. The app works without Redis - analytics are fetched live on every request with caching disabled.
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
VITE_API_URL=http://localhost:8787
Set each secret via Wrangler:
wrangler secret put SUPABASE_URL
wrangler secret put SUPABASE_SERVICE_KEY
wrangler secret put SUPABASE_ANON_KEY
wrangler secret put YOUTUBE_API_KEY
wrangler secret put INSTAGRAM_APP_ID
wrangler secret put INSTAGRAM_APP_SECRET
wrangler secret put CLOUDINARY_CLOUD_NAME
wrangler secret put CLOUDINARY_UPLOAD_PRESET
wrangler secret put BACKEND_URL
wrangler secret put FRONTEND_URL
wrangler secret put UPSTASH_REDIS_REST_URL
wrangler secret put UPSTASH_REDIS_REST_TOKENYouTube Data API v3
- Open Google Cloud Console and create a new project
- Navigate to APIs and Services then Library, search for "YouTube Data API v3", and enable it
- Go to Credentials, click Create Credentials, and select API key
- Optionally restrict the key to the YouTube Data API v3 for security
- Set it as
YOUTUBE_API_KEY
Instagram Graph API
Instagram requires a Business or Creator account linked to a Facebook Page.
- Go to Meta for Developers and create an app of type Business
- Add the Instagram Graph API product to your app
- Note your App ID and App Secret from the app's Basic Settings
- Set
INSTAGRAM_APP_IDandINSTAGRAM_APP_SECRET - In Twiligent, go to Accounts and use the Connect Instagram flow to authorize accounts via OAuth
Cloudinary (required for publishing only)
- Sign up at cloudinary.com (free tier: 25 GB storage, 25 GB bandwidth per month)
- From the Cloudinary dashboard, copy your Cloud name
- Create an unsigned upload preset in Cloudinary (Settings then Upload then Upload presets)
- Set
CLOUDINARY_CLOUD_NAMEandCLOUDINARY_UPLOAD_PRESET
Twiligent uses a dual-scheduler approach to publish Instagram posts reliably:
- Primary: The Cloudflare Worker has a built-in cron trigger that fires every 15 minutes. No setup required - it runs automatically once the Worker is deployed.
- Fallback: A GitHub Actions workflow (
publish-scheduled.yml) also runs every 15 minutes. It serves as a backup in case the Worker cron misses a cycle.
Both schedulers write to the same scheduled_posts table in Supabase. A publishing status flag acts as a mutex to prevent the same post from being published twice.
- You schedule a post through the Publish page (photo, reel, or story with caption and optional metadata)
- The post is saved to Supabase with
status: 'pending'and ascheduled_attimestamp - The Worker cron fires every 15 minutes, queries for due posts, and publishes them via the Instagram Graph API
- Each post is marked
publishingbefore the API call andpublishedorfailedafter - The GitHub Actions workflow runs the same logic as a redundant fallback
The Cloudflare Worker cron requires no additional setup. To enable the GitHub Actions fallback:
Step 1 - Add repository secrets
Go to your GitHub repo, then Settings, then Secrets and variables, then Actions, then New repository secret:
| Name | Value |
|---|---|
SUPABASE_URL |
Your Supabase project URL |
SUPABASE_SERVICE_KEY |
Your Supabase service role key |
Step 2 - Enable Actions
Go to your repo then Actions and enable workflows if prompted. The Instagram Scheduled Publisher workflow starts automatically on the cron schedule.
Free tier note: Public repos get unlimited GitHub Actions minutes. For private repos (2,000 min per month limit), change the cron in .github/workflows/publish-scheduled.yml from */15 to */30 to stay within limits.
flowchart TD
A[Browser - Cloudflare Pages] -->|HTTPS + Bearer JWT| B[Cloudflare Worker - Hono]
B -->|JWT verification| C[Supabase Auth]
B -->|Read/write data| D[(Supabase Postgres)]
B -->|Cache get/set| E[(Upstash Redis)]
B -->|YouTube Data API v3| F[Google APIs]
B -->|Instagram Graph API v25| G[Meta APIs]
B -->|Upload preset config| H[Cloudinary CDN]
B -->|Worker cron every 15 min| B
B -->|Publish scheduled posts| G
I[GitHub Actions - every 15 min] -->|Read pending posts| D
I -->|Publish fallback| G
Request lifecycle:
- The browser sends a request with a Supabase JWT in the
Authorization: Bearerheader - The Cloudflare Worker verifies the JWT and extracts the user ID
- Route handlers query Supabase using the verified user ID as an isolation filter
- Frequently accessed data (analytics) is read from Upstash Redis if available; otherwise fetched live from the platform APIs and cached
- Responses are returned as JSON; access tokens are always stripped before leaving the Worker
Cron lifecycle:
- The Cloudflare scheduler fires the Worker on a
*/15 * * * *cron - The Worker queries
scheduled_postsfor rows withstatus = 'pending'andscheduled_at <= now() - Each due post is set to
publishing(mutex), published to Instagram, then set topublishedorfailed - A separate
0 0 * * *cron refreshes expiring Instagram access tokens across all users
twiligent/
├── .github/
│ └── workflows/
│ ├── deploy-backend.yml # Wrangler deploy on backend changes
│ ├── deploy-frontend.yml # Cloudflare Pages deploy on frontend changes
│ └── publish-scheduled.yml # Scheduled post fallback (every 15 minutes)
│
├── backend/
│ ├── lib/
│ │ ├── db.js # All Supabase query functions (single source)
│ │ ├── cache.js # Redis cache helpers (silent fail on error)
│ │ ├── redis.js # Upstash Redis client factory
│ │ └── supabase.js # Supabase client factory
│ ├── middleware/
│ │ └── auth.js # requireAuth: JWT verification, sets userId
│ ├── routes/
│ │ ├── accounts.js # Add, remove, and refresh accounts
│ │ ├── analytics.js # YouTube and Instagram analytics
│ │ ├── publishing.js # Instagram Graph API publish flow
│ │ ├── scheduledPosts.js # CRUD for scheduled post queue
│ │ ├── instagramAuth.js # Instagram OAuth flow
│ │ ├── settings.js # User settings (GitHub PAT)
│ │ └── keys.js # API key presence check
│ ├── services/
│ │ ├── youtube.js # YouTube Data API v3 client
│ │ └── instagram.js # Instagram Graph API client
│ ├── server.js # Hono app + Worker export (fetch + scheduled)
│ └── wrangler.toml # Cloudflare Worker config and cron triggers
│
├── frontend/
│ └── src/
│ ├── features/
│ │ ├── analytics/
│ │ │ ├── overview/ # Unified cross-platform dashboard
│ │ │ ├── channel/ # YouTube channel deep-dive (8 tabs)
│ │ │ ├── instagram/ # Instagram account analytics
│ │ │ ├── videos/ # All videos explorer
│ │ │ ├── shorts/ # YouTube Shorts explorer
│ │ │ └── reels/ # Instagram content explorer
│ │ ├── accounts/ # Account management UI
│ │ ├── publishing/ # Single and bulk upload with scheduler
│ │ └── settings/ # GitHub PAT configuration
│ ├── context/
│ │ └── AppContext.jsx # Global accounts and loading state
│ ├── layout/
│ │ ├── Sidebar.jsx # Collapsible navigation sidebar
│ │ └── Header.jsx # Breadcrumbs, refresh, theme toggle
│ ├── components/ui/ # shadcn/ui components and custom primitives
│ └── services/
│ └── api.js # Frontend API client (all fetch calls)
│
└── scripts/
└── publish-scheduled.js # Node.js script run by GitHub Actions fallback
Frontend
| Library | Version | Purpose |
|---|---|---|
| React | 19 | UI framework |
| Vite | 7 | Build tool and dev server |
| Tailwind CSS | 4 | Utility-first styling via CSS variables |
| shadcn/ui (base-nova) | latest | Component library built on Base UI |
| Recharts | 3 | Charts and data visualization |
| React Router | 7 | Client-side routing with lazy loading |
| next-themes | 0.4 | Dark and light mode toggling |
| Lucide React | 1.17 | Icon library |
| Sonner | 2 | Toast notifications |
Backend
| Library | Version | Purpose |
|---|---|---|
| Hono | 4 | Web framework for Cloudflare Workers |
| @supabase/supabase-js | 2 | Supabase client for database and auth |
| @upstash/redis | 1 | Edge-compatible Redis client for caching |
| Wrangler | 3 | Cloudflare Workers CLI (dev and deploy) |
Infrastructure
| Service | Purpose |
|---|---|
| Cloudflare Workers | Backend API and built-in cron scheduler |
| Cloudflare Pages | Frontend SPA hosting with CI/CD |
| Supabase | PostgreSQL database and authentication (JWT) |
| Upstash Redis | Analytics caching layer (optional) |
| YouTube Data API v3 | Channel and video analytics |
| Instagram Graph API v25 | Account analytics and content publishing |
| Cloudinary | Media CDN (Instagram requires public URLs) |
| GitHub Actions | Fallback cron publisher and deployment CI/CD |
Most analytics tools share one or more of these problems:
- They charge monthly subscriptions for data you already own
- They only support one platform, forcing you to switch between dashboards
- They require you to authorize a third-party service with access to your accounts
- They lock historical data behind higher tiers
Twiligent is different on each point:
- Free to run. Uses only free API tiers, free Cloudflare Workers and Pages, and free Supabase and Upstash tiers
- Multi-platform. YouTube and Instagram in one unified view
- Self-hosted. You own every piece of the infrastructure: the Worker, the database, the cache, the credentials
- Open and auditable. Every API call is a readable service module; the database schema is plain SQL you can inspect and export at any time
Contributions are welcome. For significant changes, open an issue first to discuss the approach.
- Fork the repository from github.com/spacesdrive/twiligent
- Create a feature branch
git checkout -b feature/your-feature-name- Make your changes and commit
git commit -m "feat: add your feature description"- Push and open a pull request
git push origin feature/your-feature-namePlease keep pull requests focused: one feature or fix per PR. Avoid unrelated refactors in the same change.







