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

- Tool: App Features
- Last updated: July 2026

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

## 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** (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.
- **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.
- **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.
- **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.
- **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.
- **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.

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

```sql
create table public.scheduled_posts (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  platform text not null check (platform in ('instagram', 'facebook', 'x', 'linkedin')),
  caption text,
  media_urls jsonb default '[]'::jsonb,
  publish_at timestamptz not null,
  status text not null default 'scheduled' check (status in ('draft', 'scheduled', 'publishing', 'published', 'failed')),
  error_message text,
  created_at timestamptz not null default now()
);

alter table public.scheduled_posts enable row level security;

create policy "Users can view own posts"
  on public.scheduled_posts for select
  using (auth.uid() = user_id);

create policy "Users can insert own posts"
  on public.scheduled_posts for insert
  with check (auth.uid() = user_id);

create policy "Users can update own posts"
  on public.scheduled_posts for update
  using (auth.uid() = user_id);

create policy "Users can delete own posts"
  on public.scheduled_posts for delete
  using (auth.uid() = user_id);

create index scheduled_posts_publish_idx
  on public.scheduled_posts (publish_at, status)
  where status = 'scheduled';

create table public.platform_tokens (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  platform text not null check (platform in ('instagram', 'facebook', 'x', 'linkedin')),
  access_token text not null,
  refresh_token text,
  expires_at timestamptz,
  created_at timestamptz not null default now(),
  unique (user_id, platform)
);

alter table public.platform_tokens enable row level security;

create policy "Users can view own tokens"
  on public.platform_tokens for select
  using (auth.uid() = user_id);

create policy "Users can insert own tokens"
  on public.platform_tokens for insert
  with check (auth.uid() = user_id);

create policy "Users can update own tokens"
  on public.platform_tokens for update
  using (auth.uid() = user_id);

create policy "Users can delete own tokens"
  on public.platform_tokens for delete
  using (auth.uid() = user_id);
```

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 paths

### Lovable — fit 2/10, 3-5 hours

Lovable builds the Supabase-backed post queue UI quickly but is a web tool — it cannot produce native mobile DatePickers or a background scheduling job, making it better as a companion web admin panel than the primary mobile scheduler.

1. Create a new Lovable project, connect Lovable Cloud so the scheduled_posts table and Supabase auth are provisioned automatically
2. Paste the prompt below and let Agent Mode build the queue UI, calendar view, and status badge system
3. Open the Supabase Edge Functions section in the Cloud tab and manually add the pg_cron scheduler code — Lovable will not generate this for you
4. Publish the web panel and use it as a desktop companion to a mobile app built in FlutterFlow; both share the same Supabase project

Starter prompt:

```
Build a content scheduling web admin panel connected to Supabase. Create a 'scheduled_posts' table (id uuid pk, user_id uuid, platform text, caption text, media_urls jsonb, publish_at timestamptz, status text default 'scheduled', error_message text, created_at). Schedule Composer: a form with a platform selector (Instagram, Facebook, X, LinkedIn), a caption textarea, a media upload field (Supabase Storage, max 10 MB), and a date+time picker that converts the selected local time to UTC before saving. Scheduled Posts Calendar: a monthly calendar view showing posts as color-coded dots per day (Instagram=purple, X=black, LinkedIn=blue, Facebook=orange); clicking a day expands a list of that day's posts. Each post card shows a status badge (Scheduled=blue, Published=green, Failed=red) with a retry button on Failed. Subscribe to the scheduled_posts table via Supabase Realtime so status updates without a page refresh. Show an empty state illustration with 'Schedule your first post' CTA when the queue is empty. Protect all routes with Supabase Auth.
```

Limitations:

- Lovable produces a web app only — the mobile-native DatePicker and device time zone detection require FlutterFlow
- pg_cron and the Edge Function scheduler must be added manually in the Supabase Dashboard; Lovable does not generate backend cron infrastructure
- OAuth token exchange flows for Meta and LinkedIn require custom server-side logic that Lovable cannot scaffold

### Flutterflow — fit 4/10, 1-2 days

The best fit for this feature: FlutterFlow's native DatePicker and TimePicker widgets handle time zone-aware scheduling, its Supabase integration covers CRUD, and Custom Actions in Dart fill the OAuth and RPC gaps.

1. In FlutterFlow, add Supabase as your backend in Settings → Integrations → Supabase; run the SQL schema from this page in the Supabase SQL editor to create scheduled_posts and platform_tokens tables
2. Build the Schedule Composer page: add a DatePicker widget (set locale to device locale so it respects the user's time zone), a TimePicker widget, a TextField for caption, and an image upload via the built-in Supabase Storage upload action; write a Custom Action in Dart to convert the combined local DateTime to UTC using .toUtc() before the Supabase insert
3. Build the Calendar page using the table_calendar Flutter package added under Settings → Custom Code → Dependencies; create a page state variable holding a List<ScheduledPost> fed from a Supabase query action; map each post to a CalendarEvent color-coded by platform
4. Add Firebase Cloud Messaging: enable Firebase in Settings → Firebase and add firebase_messaging ^15 in custom dependencies; write a Custom Action to call Supabase's schedule-publisher Edge Function URL via an HTTP request and wire it to the page's onLoad event to refresh the status of due posts
5. In the Action editor for each scheduled post card, add a Delete action (Supabase row delete by id) and an Edit action navigating to the Composer page pre-filled with the post's data using page parameters
6. Test on a real device using the FlutterFlow mobile preview app — the browser preview does not support time zone detection or background notification delivery

Limitations:

- The background publish job (pg_cron + Edge Function) runs on Supabase's servers, not in the Flutter app — FlutterFlow cannot scaffold this; you must create the Edge Function manually in the Supabase Dashboard
- Custom Actions for OAuth token refresh require hand-written Dart code; FlutterFlow's visual action editor does not cover the full OAuth 2.0 refresh flow for Meta or LinkedIn
- The table_calendar package must be added as a custom dependency; FlutterFlow does not include it out of the box and occasional version conflicts with FlutterFlow's bundled Flutter version require manual resolution

### Custom — fit 5/10, 1-2 weeks

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.

1. React 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. Supabase 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. Per-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. Retry 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

Limitations:

- 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

## Gotchas

- **OAuth tokens expire mid-queue, silently failing scheduled posts** — 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** — 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** — 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** — 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** — 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

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

---

Source: https://www.rapidevelopers.com/app-features/content-scheduling
© RapidDev — https://www.rapidevelopers.com/app-features/content-scheduling
