Feature spec
IntermediateCategory
vertical-tools
Build with AI
1-2 days with FlutterFlow + Supabase
Custom build
1-2 weeks custom dev
Running cost
$0/mo up to ~100 users; $25-30/mo at 1K users
Works on
Everything it takes to ship Content Scheduling — parts, prompts, and real costs.
Content scheduling needs six pieces: a date/time picker that converts to UTC, a Supabase table for queued posts, OAuth tokens per social platform, a pg_cron Edge Function that fires every minute to auto-publish, push notifications for publish confirmation, and a calendar view of upcoming posts. With FlutterFlow + Supabase you can ship a working mobile scheduler in 1–2 days for $0/month at small scale — costs only appear when you exceed Supabase free tier or X API write limits.
What a Content Scheduling Feature Actually Is
Content scheduling lets creators and marketers draft a post, attach media, pick a future date and time, and walk away — the app handles publishing automatically. The user never has to be in the app at publish time. Under the hood this requires three distinct systems working together: a mobile-native UI for composing and queuing posts, a persistent backend job (pg_cron + Supabase Edge Function) that wakes up every minute to check for due posts, and OAuth-authenticated API calls to each social platform. The product decisions that define quality are how precisely posts fire, how gracefully token expiry and API failures surface to the user, and whether the scheduler respects the user's local time zone rather than the server's.
What users consider table stakes in 2026
- Queue drafts with a chosen publish date and time that reflects the user's local time zone — 9am means 9am in their city
- Visual calendar view of all scheduled items color-coded by platform (Instagram=purple, X=black, LinkedIn=blue)
- Edit or delete any queued post before its publish_at fires, with immediate cancellation in the backend
- Status badges showing the full lifecycle: Scheduled, Publishing, Published, Failed — with a retry button on Failed
- Push notification confirming when a post goes live or explaining exactly why it failed
- Empty state with an illustration and 'Schedule your first post' CTA so new users know what to do
Anatomy of the Feature
Six components — AI tools generate the UI and data layers well; the scheduler backend and OAuth token store are where first builds silently fail.
Schedule Composer UI
UIFlutter DatePicker and TimePicker widgets for choosing publish time, a TextEditingController for the caption/body field, and an image_picker ^1.1 integration for attaching media up to 10 MB. The composer saves to Supabase Storage first (so the file URL exists before the schedule record is created), then writes a row to the scheduled_posts table. Tapping away from a draft saves it in a 'draft' status so the user can return without losing work.
Note: The DatePicker must convert the user's local selection to UTC before inserting into Supabase. Never store local time — pg_cron runs on UTC and will fire at the wrong wall-clock time if you do.
Scheduled Posts List / Calendar
UIA table_calendar ^3.1 widget rendering all scheduled_posts grouped by date, with color-coded status chips: Scheduled (blue), Published (green), Failed (red). A Supabase Realtime subscription on the scheduled_posts table pushes live status updates to the calendar without requiring a manual refresh — so when the Edge Function marks a post as Published, the status chip updates in real time.
Note: flutter_calendar_carousel is an alternative but table_calendar has more active maintenance as of 2026 and handles the event-per-day density better for creators scheduling multiple daily posts.
Scheduler Backend Job
BackendA Supabase Edge Function (Deno runtime) invoked by the pg_cron extension on a one-minute schedule. The function queries SELECT * FROM scheduled_posts WHERE publish_at <= now() AND status = 'scheduled' LIMIT 10, processes each post by calling the appropriate social platform API, then updates status to 'published' or 'failed' with an error_message. Keeping LIMIT 10 prevents a single pg_cron invocation from timing out when many posts are due simultaneously.
Note: pg_cron has ±1 minute resolution — set user expectations in the UI ('Posts publish within 1 minute of scheduled time'). Never promise exact-second delivery. Cold starts on the Edge Function can add 50-500 ms on top of this.
Social Platform API Connector
ServicePer-platform API clients inside the Edge Function: Meta Graph API for Instagram and Facebook scheduled publishing, X API v2 POST /2/tweets (requires Basic plan for the scheduled_for field), and LinkedIn Marketing API v2/ugcPosts with scheduledPublishTime. Each call uses the stored OAuth access token for that user and platform. The connector checks token expiry before calling and attempts a refresh if expires_at is within 7 days.
Note: Meta Graph API requires App Review for publish_to_groups and pages permissions — budget 1-4 weeks for review. LinkedIn Marketing API requires Marketing Developer Platform access. Build with Instagram personal account first while review is pending.
OAuth Token Store
DataA Supabase platform_tokens table holding one row per user per platform: user_id, platform, access_token encrypted via pgcrypto, refresh_token, and expires_at. The Edge Function reads this table before every publish attempt, refreshes the token if needed, and writes back the new access_token and expires_at. The app sends a 'reconnect your account' push notification 7 days before any token expires.
Note: Meta and LinkedIn tokens have a 60-day rolling expiry. If the user never opens the app, the refresh logic never fires from the client side — the Edge Function pre-publish refresh is the only safety net.
Push Notification Trigger
ServiceFirebase Cloud Messaging (FCM) via the firebase_messaging ^15 Dart SDK on the client and an FCM HTTP v1 API call from inside the Supabase Edge Function on publish success or failure. The Edge Function uses the FCM service account key stored in Supabase Secrets (never in code). The notification body for failures includes the platform and error reason so the user knows immediately whether to reconnect their account or retry.
Note: FCM is free with no per-message cost on the Spark plan up to ~500K messages/day. Supabase Secrets (not environment variables in code) is the correct place to store the FCM service account JSON.
The data model
Two tables with RLS scoped to auth.uid(). Run this in the Supabase SQL editor — it creates the scheduled_posts table, the platform_tokens table, their policies, and a performance index on publish_at for efficient pg_cron queries.
1create table public.scheduled_posts (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 platform text not null check (platform in ('instagram', 'facebook', 'x', 'linkedin')),5 caption text,6 media_urls jsonb default '[]'::jsonb,7 publish_at timestamptz not null,8 status text not null default 'scheduled' check (status in ('draft', 'scheduled', 'publishing', 'published', 'failed')),9 error_message text,10 created_at timestamptz not null default now()11);1213alter table public.scheduled_posts enable row level security;1415create policy "Users can view own posts"16 on public.scheduled_posts for select17 using (auth.uid() = user_id);1819create policy "Users can insert own posts"20 on public.scheduled_posts for insert21 with check (auth.uid() = user_id);2223create policy "Users can update own posts"24 on public.scheduled_posts for update25 using (auth.uid() = user_id);2627create policy "Users can delete own posts"28 on public.scheduled_posts for delete29 using (auth.uid() = user_id);3031create index scheduled_posts_publish_idx32 on public.scheduled_posts (publish_at, status)33 where status = 'scheduled';3435create table public.platform_tokens (36 id uuid primary key default gen_random_uuid(),37 user_id uuid references auth.users(id) on delete cascade not null,38 platform text not null check (platform in ('instagram', 'facebook', 'x', 'linkedin')),39 access_token text not null,40 refresh_token text,41 expires_at timestamptz,42 created_at timestamptz not null default now(),43 unique (user_id, platform)44);4546alter table public.platform_tokens enable row level security;4748create policy "Users can view own tokens"49 on public.platform_tokens for select50 using (auth.uid() = user_id);5152create policy "Users can insert own tokens"53 on public.platform_tokens for insert54 with check (auth.uid() = user_id);5556create policy "Users can update own tokens"57 on public.platform_tokens for update58 using (auth.uid() = user_id);5960create policy "Users can delete own tokens"61 on public.platform_tokens for delete62 using (auth.uid() = user_id);Heads up: The partial index on (publish_at, status) WHERE status = 'scheduled' keeps pg_cron queries fast even with tens of thousands of historical published rows in the table. The Edge Function service role key (not the anon key) is needed to read platform_tokens from server-side code — store it in Supabase Secrets.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Required when publishing to 3+ platforms simultaneously with per-platform rate-limit handling, exponential back-off on failures, per-user approval workflows, or bulk scheduling from CSV imports.
Step by step
- 1React Native (Expo) or Flutter app with a fully custom DateTimePicker wired to the device locale via Intl.DateTimeFormat or the intl Dart package for bulletproof time zone conversion
- 2Supabase Edge Function queue architecture: instead of fan-out in one call, write one scheduled_posts_queue row per platform per post; pg_cron picks up at most 10 rows per minute; failures get an exponential back-off column (next_retry_at) rather than a simple 'failed' status
- 3Per-platform token refresh middleware as a shared Edge Function invoked before every publish attempt — handles Meta 60-day refresh, LinkedIn 60-day refresh, and X OAuth 2.0 PKCE token rotation atomically
- 4Retry logic with dead-letter queue: after 3 failed attempts, move the row to a scheduled_posts_failed table with the full error history and send a 'please reconnect' deep-link push notification
Where this path bites
- Two to three times the build time compared to FlutterFlow for the same UI surface — justified only when the retry/approval/bulk-import complexity genuinely requires it
Third-party services you'll need
Six services power this feature. Supabase is the only paid one at scale — social APIs and FCM are free at most volumes founders will encounter.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database (scheduled_posts, platform_tokens), Auth, Storage (media attachments), Edge Functions (publisher), pg_cron (trigger) | 2 projects, 500 MB DB, 5 GB storage, 500K Edge Function invocations/month | Pro $25/mo (8 GB DB, unlimited invocations) |
| Meta Graph API | Publish to Instagram and Facebook Pages on schedule | Free with approved app (no per-call cost) | Free — but requires App Review for pages/groups permissions (approx 1-4 weeks) |
| X API v2 | Post tweets with the scheduled_for field | Free tier: 1,500 tweets/month write limit; scheduled_for field NOT available on free tier | Basic $100/mo for higher write limits and scheduled_for support |
| LinkedIn Marketing API | Schedule posts via v2/ugcPosts with scheduledPublishTime | Free with OAuth app approval | Free — requires Marketing Developer Platform access (approx review required) |
| Firebase Cloud Messaging (FCM) | Push notifications on publish success or failure | Free, no per-message cost, up to ~500K messages/day on Spark plan | Free (Spark plan sufficient) |
| FlutterFlow | Visual mobile app builder for the Composer and Calendar UI | Free plan (limited features, no code export) | Standard $30/mo (no code export) or Pro $70/mo (code export + versioning) |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Supabase free tier covers DB, Edge Function invocations, and 5 GB storage easily at this volume. FCM is free. All social API tiers are free at low post counts. pg_cron runs within the free invocation limit.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
OAuth tokens expire mid-queue, silently failing scheduled posts
Symptom: Meta and LinkedIn access tokens have a 60-day rolling expiry. If a user connects their account and then doesn't open the app for two months, the token expires quietly. When pg_cron fires the Edge Function at publish time, the social API returns a 401 — the post is marked Failed, and the user has no idea why unless they open the app and check. The client-side token refresh logic in FlutterFlow never runs because the app is closed.
Fix: Store expires_at in the platform_tokens table. Add a pre-publish refresh step inside the Edge Function: if expires_at is within 7 days, attempt a refresh before calling the social API. Separately, query platform_tokens nightly for tokens expiring within 7 days and send a 'Reconnect your Instagram account' FCM push with a deep-link to the OAuth flow. Never assume the app will be open.
pg_cron fires up to 1 minute late, triggering user complaints
Symptom: pg_cron has ±1 minute resolution — it wakes up on the minute, not on the second. A post scheduled for 9:00:00 may publish at 9:00:45 or 9:01:30. Social platform APIs themselves also accept posts a few seconds after the stated time. Users who expect exact-second delivery will file support tickets.
Fix: Set the expectation in the UI at scheduling time: display 'Posts publish within 1 minute of the scheduled time' next to the time picker. Never use language like 'will publish at exactly 9:00am'. For creators where second-level precision matters, this is a custom-dev requirement, not a FlutterFlow + pg_cron use case.
FlutterFlow Custom Actions silently swallow Supabase RPC errors
Symptom: When a Custom Action in FlutterFlow calls a Supabase RPC function and the call fails (network error, RLS violation, or Edge Function 500), FlutterFlow's default exception handling in Custom Actions does nothing — no snackbar, no status update, no error log. The user sees their post sit in 'Scheduled' status forever because the 'failed' write never happened.
Fix: Always wrap Supabase RPC and insert calls in try/catch inside your Custom Action Dart code. In the catch block, explicitly update the scheduled_posts row to status='failed' and error_message='[error details]', then call showSnackbar() with a user-readable message. Test error paths by intentionally passing invalid data before shipping.
Supabase Edge Function times out during multi-platform fan-out
Symptom: If the pg_cron Edge Function tries to publish one post to Instagram, Facebook, X, and LinkedIn in a single invocation — four sequential HTTP calls — and any one of them is slow (LinkedIn in particular averages 1-3 seconds per call), the function can exhaust its 150-second wall-clock limit before completing. The post ends up in an ambiguous state: some platforms published, others not.
Fix: Write one row per platform per post into a scheduled_posts_queue table instead of fanning out in a single function call. Each pg_cron invocation processes LIMIT 10 queue rows, one API call per row. This keeps each invocation fast and makes partial failures recoverable — only the failed platform row gets status='failed'; the others are unaffected.
Deleted draft media accumulates in Supabase Storage, growing your bill
Symptom: When a user uploads an image or video to Supabase Storage as part of composing a post and then deletes the draft before scheduling, or deletes a scheduled post before it publishes, the Storage object is orphaned — the scheduled_posts row is deleted but the file remains. Over months, thousands of abandoned media files accumulate and inflate your Storage bill.
Fix: Create a Supabase Database Webhook on the scheduled_posts table for DELETE events. The webhook triggers a separate Edge Function that reads the deleted row's media_urls jsonb field and calls the Supabase Storage delete API for each URL. Test this webhook by creating and immediately deleting a post with an attached file and verifying the Storage bucket no longer contains it.
Best practices
Always convert the user's selected date and time to UTC before writing to Supabase — store UTC, display local; pg_cron runs on UTC and will fire at the wrong time if you store local time
Save draft state automatically whenever the user leaves the Composer screen so they never lose a half-composed post; use a 'draft' status in scheduled_posts and only flip to 'scheduled' when they explicitly tap the Schedule button
Pre-sign media URLs in Supabase Storage before storing them in media_urls — signed URLs expire, so generate a fresh signed URL at publish time inside the Edge Function rather than storing a signed URL at draft time
Add a 7-day pre-expiry push notification for every platform_tokens row so users reconnect accounts before the Edge Function hits a 401 mid-queue
Show a real-time Realtime subscription status indicator on the Calendar screen so users see live status updates without needing to pull-to-refresh
Rate-limit the retry logic on Failed posts — wait at least 15 minutes before auto-retrying, and cap retries at 3 attempts before moving to a 'dead letter' state that requires user intervention
Surface the exact error_message from the platform API in the UI on Failed posts ('Instagram: media aspect ratio not supported') — generic 'publish failed' messages cause unnecessary reconnect flows
Test the full end-to-end flow with a post scheduled 2 minutes in the future on a staging Supabase project before deploying to production — pg_cron, Edge Function, OAuth, and FCM must all be verified together
When You Need Custom Development
FlutterFlow + Supabase covers the core scheduling loop comfortably. These signals indicate the complexity has grown beyond what visual tools reliably handle:
- Publishing to more than 2-3 social platforms simultaneously with per-platform rate-limit tracking, exponential back-off on failures, and a retry dead-letter queue — this logic cannot be built reliably inside FlutterFlow Custom Actions
- An approval workflow where a manager must review and approve scheduled content before it auto-publishes, requiring a multi-step status machine (Draft → Pending Review → Approved → Scheduled → Published) with role-based access control
- Bulk scheduling via CSV import — uploading 100+ posts at once, validating each row, and distributing them across the queue without hitting any platform's hourly rate limit requires server-side logic with backpressure that FlutterFlow cannot scaffold
- An analytics feedback loop that auto-reschedules underperforming content at better engagement windows based on historical per-platform performance data — this requires a separate analytics pipeline and ML-based scoring, not just a CRUD schedule
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Can I schedule posts to Instagram directly from my app, or do I need a third-party service?
You can publish directly using the Meta Graph API — no third-party scheduler needed. Your app stores the user's OAuth token and your Supabase Edge Function calls the Instagram publish endpoint at the scheduled time. The catch: Meta requires App Review before your app can publish to Pages or Groups (personal accounts work in development mode). Budget 1-4 weeks for review approval before going live.
Does content scheduling work offline — what happens if the phone has no signal at publish time?
Scheduling is server-side, not phone-side. Once a post is queued in Supabase, the pg_cron job fires the Edge Function on schedule whether the user's phone is on, offline, or turned off. The phone's connectivity only matters for composing and queuing posts, and for receiving the FCM push notification after publishing. A user in a tunnel at 9am will still have their post go live — they'll just get the notification later when they reconnect.
How do I handle time zones so a post scheduled for 9am fires at 9am in the user's city?
The Flutter DatePicker returns a DateTime in the device's local time zone. Before inserting into Supabase, convert it to UTC with dateTime.toUtc() in your Custom Action. Store UTC in the timestamptz column — Postgres and pg_cron both operate in UTC. When displaying scheduled times back to the user, convert from UTC to local using DateTime.fromMillisecondsSinceEpoch(...).toLocal(). Never store local time in the database.
What's the cheapest way to add content scheduling without paying for a separate SaaS?
Supabase + pg_cron + Meta Graph API is entirely self-hosted with no per-post fees. At under 100 users the total cost is $0/month — Supabase's free tier covers the database, Edge Functions, and storage. You only pay when you scale past the free tier (Supabase Pro $25/mo) or if your users post enough on X to exceed the 1,500 tweet/month free write limit ($100/mo for X Basic).
How does the scheduler know when to publish — does the phone have to be open?
No. The publishing runs entirely on Supabase's servers via a pg_cron job that wakes up every minute and calls an Edge Function. The Edge Function checks for posts with publish_at <= now() and status = 'scheduled', then calls the social platform APIs using stored OAuth tokens. The user's phone is only involved in receiving the FCM push notification confirming success or failure — and even that is delivered whenever the phone comes online.
Can users edit a scheduled post after they've queued it?
Yes, as long as the post hasn't started publishing yet. In your FlutterFlow app, tap the post on the calendar to open the Composer pre-filled with its data. Edits update the scheduled_posts row in Supabase (caption, media, publish_at). One edge case to handle: if the user tries to edit a post within 30 seconds of its publish_at, the Edge Function may already be mid-flight. Check status in the update logic — only allow edits when status is 'scheduled', not 'publishing'.
What happens if the social platform API is down at the scheduled publish time?
The Edge Function catches the API error, writes status='failed' and error_message to the scheduled_posts row, and triggers an FCM push notification explaining the failure. The Supabase Realtime subscription on the calendar screen updates the status badge to red in real time. You should build a retry button on Failed posts that re-queues the post for publishing 15 minutes later — and cap retries at 3 before requiring the user to take action.
Is there a free-tier path for a small creator app with under 100 users?
Yes — the entire stack runs on free tiers at this scale. Supabase free tier (500 MB DB, 5 GB storage, 500K Edge Function invocations) comfortably handles 100 users posting several times per day. FCM is free. Meta Graph API and LinkedIn Marketing API have no per-call cost after OAuth approval. X API's free tier allows 1,500 tweets per month across all users, which is ample for a small app. Total cost: $0/month until you grow.
Need this feature production-ready?
RapidDev builds content scheduling into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.