Multi-user SaaS: people sign up, upload a batch of videos, set one daily post time, and DRIPFEED auto-posts the next video in their queue to Instagram every day — with an AI-generated caption + hashtags.
frontend/index.html -> single-file app (Firebase Auth + Firestore + Storage), deploy anywhere static (GitHub Pages works great)
worker/ -> Cloudflare Worker: Instagram OAuth handoff + the daily cron that posts
firestore.rules
storage.rules
- Frontend talks directly to Firebase (Auth, Firestore, Storage) for everything except Instagram connection — sign-up, uploading videos, editing the schedule, viewing the queue. No backend needed for any of that.
- Worker holds the three secrets the frontend must never see (Composio
key, OpenAI key, Firebase service-account key) and does two things:
- Two HTTP routes that hand the user off to Composio's hosted Instagram OAuth flow and record the result.
- A cron job (every 15 min) that checks which users' daily post time has arrived, generates a caption with OpenAI, and publishes the next queued video as a Reel through Composio.
- Create a project at console.firebase.google.com.
- Enable Authentication → Email/Password.
- Enable Firestore (production mode) and Storage.
- Deploy the rules in this repo:
firebase deploy --only firestore:rules,storage:rules - Firestore will ask you to create a composite index the first time the
Worker runs its cron query (
igConnected == true+nextPostAt <= now). The error it throws contains a direct "create index" link — click it once. - Create a service account for the Worker: Project Settings → Service
Accounts → Generate new private key. You'll use its
client_emailandprivate_keyas Worker secrets below. - Copy your web app config (Project Settings → General → Your apps) into
frontend/index.html'sfirebaseConfigblock.
- Sign up at composio.dev and create a project.
- In the dashboard, add the Instagram toolkit and create an auth config
for it (this is what lets your users connect their own Instagram
accounts). Copy its id — that's
COMPOSIO_INSTAGRAM_AUTH_CONFIG_ID. - Grab a project API key — that's
COMPOSIO_API_KEY. - Before going live, confirm the exact Instagram tool slugs used in
worker/src/composio.js— Composio's naming can shift:Updatecurl "https://backend.composio.dev/api/v3.1/tools?toolkit_slug=instagram" \ -H "x-api-key: $COMPOSIO_API_KEY"TOOL_SLUGSincomposio.jsif the container-create / publish / status tool names differ from what's there. - Instagram itself only allows professional (Business or Creator)
accounts linked to a Facebook Page to publish via the API — personal
accounts can't connect. Your users will need that set up on their end.
For a public multi-user app, Meta also requires app review for the
instagram_content_publishpermission before non-test users can connect; budget time for that before launch.
Create an API key at platform.openai.com. That's OPENAI_API_KEY.
cd worker
npm install
npx wrangler login
npx wrangler secret put GCP_CLIENT_EMAIL
npx wrangler secret put GCP_PRIVATE_KEY
npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put COMPOSIO_API_KEY
npx wrangler secret put COMPOSIO_INSTAGRAM_AUTH_CONFIG_ID
# edit wrangler.toml [vars]: APP_URL, WORKER_URL, GCP_PROJECT_ID
npx wrangler deploy
APP_URL must match wherever you host frontend/index.html (used for CORS
and post-connect redirects). WORKER_URL is the *.workers.dev URL you get
after your first deploy (or a custom route, if you set one up).
Fill in firebaseConfig and WORKER_URL at the top of
frontend/index.html, then push it to GitHub Pages (or any static host) —
it's a single file, no build step.
users/{uid}
email, dailyPostTime ("09:00"), timezone (IANA string), niche
igConnected, igUsername, composioConnectedAccountId, composioConnectionId
nextPostAt (UTC timestamp, Worker-owned)
users/{uid}/queue/{videoId}
fileName, storagePath, downloadUrl, note, uploadedAt
status: "queued" | "posted" | "failed"
caption, hashtags, igMediaId, postedAt / failedAt / errorMessage
The queue is FIFO by uploadedAt — each cron run takes the oldest queued
video for a due user, posts it, and marks it posted (or failed, with the
error visible in the UI, on the next queued item left untouched for the next
day's run).
- Instagram's publishing API needs the video reachable at a public HTTPS URL
during the container-create call — the Firebase Storage download URL used
here works, but Storage rules currently make
videos/**world-readable so Meta's servers can fetch it. If that's a concern, swap it for short-lived signed URLs generated by the Worker instead. - The cron runs every 15 minutes, so a post can land up to ~15 minutes after the exact scheduled time — fine for a daily cadence, but worth knowing.
- One video posted per user per due cycle, so a backlog never floods
Instagram — if the queue is empty when it's a user's turn, that day is
skipped and
lastSkipReason: "queue_empty"is recorded on their user doc.