Skip to main content
RapidDev - Software Development Agency
App Featuresvertical-tools22 min read

How to Add Content Scheduling to Your App — Copy-Paste Prompts Inside

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.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

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

Mobile

Everything it takes to ship Content Scheduling — parts, prompts, and real costs.

TL;DR

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.

Layers:UIDataBackendService

Schedule Composer UI

UI

Flutter 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

UI

A 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

Backend

A 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

Service

Per-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

Data

A 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

Service

Firebase 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.

schema.sql
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);
12
13alter table public.scheduled_posts enable row level security;
14
15create policy "Users can view own posts"
16 on public.scheduled_posts for select
17 using (auth.uid() = user_id);
18
19create policy "Users can insert own posts"
20 on public.scheduled_posts for insert
21 with check (auth.uid() = user_id);
22
23create policy "Users can update own posts"
24 on public.scheduled_posts for update
25 using (auth.uid() = user_id);
26
27create policy "Users can delete own posts"
28 on public.scheduled_posts for delete
29 using (auth.uid() = user_id);
30
31create index scheduled_posts_publish_idx
32 on public.scheduled_posts (publish_at, status)
33 where status = 'scheduled';
34
35create 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);
45
46alter table public.platform_tokens enable row level security;
47
48create policy "Users can view own tokens"
49 on public.platform_tokens for select
50 using (auth.uid() = user_id);
51
52create policy "Users can insert own tokens"
53 on public.platform_tokens for insert
54 with check (auth.uid() = user_id);
55
56create policy "Users can update own tokens"
57 on public.platform_tokens for update
58 using (auth.uid() = user_id);
59
60create policy "Users can delete own tokens"
61 on public.platform_tokens for delete
62 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.

Hand-built by developersFit for this feature:

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

  1. 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
  2. 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
  3. 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
  4. 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.

ServiceWhat it doesFree tierPaid from
SupabaseDatabase (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/monthPro $25/mo (8 GB DB, unlimited invocations)
Meta Graph APIPublish to Instagram and Facebook Pages on scheduleFree with approved app (no per-call cost)Free — but requires App Review for pages/groups permissions (approx 1-4 weeks)
X API v2Post tweets with the scheduled_for fieldFree tier: 1,500 tweets/month write limit; scheduled_for field NOT available on free tierBasic $100/mo for higher write limits and scheduled_for support
LinkedIn Marketing APISchedule posts via v2/ugcPosts with scheduledPublishTimeFree with OAuth app approvalFree — requires Marketing Developer Platform access (approx review required)
Firebase Cloud Messaging (FCM)Push notifications on publish success or failureFree, no per-message cost, up to ~500K messages/day on Spark planFree (Spark plan sufficient)
FlutterFlowVisual mobile app builder for the Composer and Calendar UIFree 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

$0/mo

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

1

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

2

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

3

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

4

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

5

Show a real-time Realtime subscription status indicator on the Calendar screen so users see live status updates without needing to pull-to-refresh

6

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

7

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

8

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.

Get a fixed-price quote

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.

RapidDev

Need this feature production-ready?

RapidDev builds content scheduling into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.