Feature spec
IntermediateCategory
communication-social
Build with AI
1-2 days with Lovable or V0
Custom build
1-2 weeks custom dev
Running cost
$0-25/mo up to 100 users · $100-300/mo at 10K users
Works on
Everything it takes to ship Social Media Post Scheduling — parts, prompts, and real costs.
Post scheduling needs five pieces: a composer with per-platform character counters, a Supabase scheduled_posts table with a status machine, encrypted OAuth token storage per connected account, a cron job (pg_cron or Vercel Cron) that publishes due posts every minute, and retry logic for failures. Ship a working scheduler in 1-2 days with Lovable or V0 for $0-25/month. OAuth token refresh race conditions and DST-aware timezone storage are the two patterns AI tools most often get wrong.
What a Social Media Post Scheduling Feature Actually Requires
Post scheduling is deceptively complex because it spans five systems that must work in concert: a composer UI with platform-specific constraints, OAuth token management for each user's connected social accounts, a job queue that survives server restarts, a cron executor that fires at the right time in the user's timezone, and platform API calls that can fail and need retrying. AI tools generate the composer and the Supabase schema well. The OAuth token storage and refresh logic, timezone conversion, and retry-on-failure patterns are where every first build has production bugs. The key architectural decision: never store UTC offset — always store the timezone name ('America/New_York') so the scheduler survives Daylight Saving Time changes.
What users consider table stakes in 2026
- Per-platform character counter that updates live as the user types — Twitter/X shows remaining characters, LinkedIn and Instagram show a softer warning near their limits
- Calendar or timeline view of all scheduled posts with color-coding by platform so users can see their content schedule at a glance
- Select one or multiple target platforms per post with platform-specific validation (Instagram requires at least one image, Twitter enforces 280 chars)
- Image and video attachment support with a preview thumbnail shown in the composer and in the calendar view
- Timezone-aware datetime picker that shows the user their local time and stores UTC in the database
- Edit or cancel a scheduled post before it fires with an immediate visual update in the calendar
- Post status tracking visible to the user: 'Scheduled' → 'Publishing' → 'Published' or 'Failed' with the error reason shown on failure
Anatomy of the Feature
Six components. The composer and calendar UI generate well with AI tools. The scheduling cron job, OAuth token refresh, and timezone handling are where production builds consistently fail on first deploy.
Post composer
UIshadcn/ui Textarea with per-platform character counter. Twitter/X: 280-char hard limit (red counter under 20 chars remaining). LinkedIn: 3,000-char soft limit. Instagram: 2,200-char limit. Platform multi-select checkboxes with platform icons. Image upload to Supabase Storage with thumbnail preview. Zod validation prevents scheduling in the past and blocks Instagram posts without an image. Timezone-aware datetime picker using date-fns-tz.
Note: Implement platform validation as separate Zod refinements so the error message names the specific platform that failed, not a generic 'invalid post' message.
Scheduling engine
BackendSupabase Edge Function running on a pg_cron schedule (every minute: '* * * * *'). Queries scheduled_posts WHERE scheduled_at <= now() AND status = 'scheduled' LIMIT 10. For each due post, calls the platform API with the stored OAuth token, updates status to 'publishing' before the API call, then sets 'published' on success or 'failed' with error_message on failure. Increments retry_count on failure and reschedules 5 minutes later up to 3 retries.
Note: Set status to 'publishing' BEFORE the API call to act as a distributed lock — if the cron runs twice in the same minute (which Supabase pg_cron can do on heavy load), the second run skips rows already in 'publishing' state.
Platform OAuth connection
ServiceOAuth 2.0 authorization code flow per platform. Twitter/X uses PKCE (Proof Key for Code Exchange). LinkedIn, Facebook, and Instagram use standard authorization code. The Supabase Edge Function handles the OAuth callback, exchanges the code for tokens, and stores them in the user_oauth_tokens table with expires_at. Token refresh: check expires_at 5 minutes before use; if expired or expiring, call the platform's token refresh endpoint first.
Note: OAuth redirect URIs must exactly match the registered URL in each platform's developer console. In Lovable, the redirect URI must be the published URL, not the preview URL. Store access_token and refresh_token as encrypted text in Supabase — never in localStorage.
Post queue
DataSupabase scheduled_posts table: id, user_id, content, platforms (text array), media_urls (text array), scheduled_at (timestamptz, stored as UTC), timezone (text, e.g. 'America/New_York'), status enum (draft/scheduled/publishing/published/failed), error_message, retry_count, published_at, created_at. RLS ensures users can only access their own posts.
Note: Always store scheduled_at in UTC and a separate timezone column with the IANA timezone name. Never store a UTC offset integer — offsets change with DST, causing posts to fire at the wrong local time after a DST boundary.
Calendar view
UIreact-big-calendar or @fullcalendar/react renders scheduled posts on a calendar grid, color-coded by platform. Drag-to-reschedule updates the scheduled_at column via a Server Action and resets status to 'scheduled' if the post was in 'failed' state. Month/week/day views. Clicking a post opens an edit modal.
Note: Import the calendar library's CSS before Tailwind global styles to avoid reset conflicts. On Tailwind v4 (V0 default), calendar library styles are frequently overridden by the CSS reset — add @layer base workarounds or scope calendar styles with a custom prefix.
Webhook / callback handler
BackendSupabase Edge Function endpoint at /functions/v1/social-callback receives platform webhooks (where available) that confirm a post was published or report a delayed failure. Updates the scheduled_posts row status to 'published' with published_at timestamp or 'failed' with the platform's error code as error_message. Triggers an in-app notification to the user.
Note: Not all platforms support webhooks for post status — Twitter/X and LinkedIn do not. For these platforms, poll the post status endpoint 30 seconds after the publish API call returns success to confirm the post is live.
The data model
Two tables: scheduled_posts (the job queue) and user_oauth_tokens (encrypted platform credentials). Run this in the Supabase SQL Editor (Dashboard → SQL Editor):
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 content text not null,5 platforms text[] not null,6 media_urls text[] not null default '{}',7 scheduled_at timestamptz not null,8 timezone text not null,9 status text not null default 'draft'10 check (status in ('draft', 'scheduled', 'publishing', 'published', 'failed')),11 error_message text,12 retry_count integer not null default 0,13 published_at timestamptz,14 created_at timestamptz not null default now(),15 updated_at timestamptz not null default now()16);1718create table public.user_oauth_tokens (19 id uuid primary key default gen_random_uuid(),20 user_id uuid references auth.users(id) on delete cascade not null,21 platform text not null,22 access_token text not null,23 refresh_token text,24 expires_at timestamptz,25 scope text,26 platform_user_id text,27 platform_username text,28 connected_at timestamptz not null default now(),29 revoked_at timestamptz,30 constraint user_oauth_tokens_user_platform_unique unique (user_id, platform)31);3233alter table public.scheduled_posts enable row level security;34alter table public.user_oauth_tokens enable row level security;3536create policy "Users can manage their own scheduled posts"37 on public.scheduled_posts for all38 using (auth.uid() = user_id)39 with check (auth.uid() = user_id);4041create policy "Users can manage their own OAuth tokens"42 on public.user_oauth_tokens for all43 using (auth.uid() = user_id)44 with check (auth.uid() = user_id);4546create index scheduled_posts_user_id_idx47 on public.scheduled_posts (user_id, scheduled_at desc);4849create index scheduled_posts_due_idx50 on public.scheduled_posts (scheduled_at)51 where status = 'scheduled';5253create index user_oauth_tokens_user_platform_idx54 on public.user_oauth_tokens (user_id, platform)55 where revoked_at is null;Heads up: The scheduled_posts_due_idx partial index covers only rows with status = 'scheduled', making the cron query fast even with millions of historical posts in the table. The user_oauth_tokens table uses a unique constraint on (user_id, platform) so each user has one active token per platform — reconnecting the same platform updates the existing row rather than creating duplicates.
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.
Full control over retry logic, token refresh with distributed locking, platform rate limit tracking, multi-step approval workflows, and analytics on post performance.
Step by step
- 1Implement advisory locking (pg_advisory_xact_lock) in the publish Edge Function to prevent concurrent token refresh race conditions when multiple posts for the same user fire simultaneously
- 2Add a rate_limits table tracking API calls per platform per user with a retry_after timestamp; surface 'rate_limited' as a post status so users understand why their post is delayed
- 3Build a team approval workflow: posts from team members require a manager review before scheduling; add an approver_id column and approved_at timestamp with email notification on pending approval
- 4Integrate post performance metrics: for platforms that support analytics APIs (Twitter/X, LinkedIn, Instagram), fetch engagement metrics (likes, shares, impressions) 24 hours after publish and store in a post_metrics table linked to scheduled_posts
Where this path bites
- Each additional social platform (Pinterest, TikTok, YouTube) requires platform-specific OAuth implementation, API client, and error handling — budget 2-3 days per new platform beyond the core four
Third-party services you'll need
The scheduler core runs on Supabase and Vercel free tiers. Platform API costs are the variable: Twitter/X charges for the API at higher posting volumes, LinkedIn and Facebook/Instagram API access is free but requires business verification:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database (scheduled_posts + oauth_tokens), pg_cron for Edge Function scheduling, Storage for post media | Free — 500 concurrent connections, 1GB storage, 500K Edge Function invocations/mo | Pro $25/mo — 10,000 concurrent connections, 100GB storage |
| Twitter/X API v2 | Post publishing on behalf of users to Twitter/X | Free tier limited to 1,500 posts/15 minutes per app (very restricted) | Basic $100/mo — higher rate limits (approx) |
| LinkedIn API | Post publishing to LinkedIn pages and personal profiles | Free — requires Marketing Developer Platform approval | Free with API access |
| Facebook/Instagram Graph API | Post publishing to Facebook Pages and Instagram Business accounts | Free — requires business verification and Meta app review | Free |
| Vercel Cron Jobs | Trigger the /api/publish-scheduled route every minute to process due posts in the V0/Next.js path | Free — 1 cron job per project | Pro $20/mo — multiple cron jobs, higher frequency |
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 scheduling at this scale. Platform APIs free at low volume. Vercel free cron. The only cost may be Twitter/X Basic API ($100/mo) if users post to Twitter frequently — avoidable by starting with LinkedIn and Instagram only.
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 redirect fails in Lovable preview — works only on published URL
Symptom: Social platforms (Twitter/X, LinkedIn, Facebook) register an exact redirect URI in their developer console. Lovable's preview URL (something-preview.lovable.app) differs from the published URL (something.lovable.app). When a user clicks 'Connect Twitter' in the preview, the OAuth callback fires to the preview URL, which is not registered, causing an immediate 'redirect_uri_mismatch' error from the platform.
Fix: Register both the preview URL AND the published URL as allowed redirect URIs in each platform's developer console. Alternatively, test OAuth connections only on the published URL and add a note in the preview UI: 'Account connection available in the published app'. Document which URL is registered where to avoid confusion later.
Token refresh race condition corrupts OAuth tokens
Symptom: When two posts for the same user are scheduled at the same minute, the cron job picks up both simultaneously and both Edge Function invocations check token expiry at the same moment. Both see an expired token, both call the platform refresh endpoint, and the first successful refresh invalidates the token the second one received. The second post fails with 'invalid_token' and, critically, the good refreshed token may be overwritten by the stale one from the second invocation.
Fix: Use a database advisory lock before token refresh: call SELECT pg_advisory_xact_lock(user_id_as_bigint) at the start of the publish transaction. Only one Edge Function invocation holds the lock at a time; the second waits, then reads the already-refreshed token. Alternatively, use an optimistic version counter on user_oauth_tokens: increment a version column on each refresh and use an UPDATE ... WHERE version = $known_version to detect concurrent updates.
Timezone offset bug fires posts at wrong time after DST change
Symptom: Storing a UTC offset integer like -300 (for UTC-5) instead of the timezone name 'America/New_York' produces correct results in winter but fires posts 1 hour late after Daylight Saving Time when the offset changes to -240. A user who schedules '9:00 AM every Monday' in November will get posts at 10:00 AM in March — without any error message.
Fix: Always store the IANA timezone name (e.g., 'America/New_York') in a timezone column. When converting local time to UTC for scheduled_at, use date-fns-tz parseInTimeZone(localDateString, timezoneIANA). When displaying scheduled_at back to the user, use formatInTimeZone(utcDate, timezoneIANA, 'MMM d, h:mm a'). Detect the user's timezone from Intl.DateTimeFormat().resolvedOptions().timeZone on the client.
Platform API rate limits cause silent post failures
Symptom: Twitter/X Basic API allows 1,500 posts/15 minutes per app (not per user). At scale, if many users' posts are scheduled in the same 15-minute window, the cron job exhausts the rate limit budget. Later posts fail with a 429 response, their status is set to 'failed', and users see an unhelpful 'post failed' error with no explanation of when to retry.
Fix: Add a rate_limits table: (platform text, window_start timestamptz, call_count int). Before each publish API call, check and increment the counter atomically. If count >= limit, set post status to 'rate_limited' with a retry_after timestamp. Show a user-facing message: 'Your Twitter post is queued — rate limit reached, will retry at 10:15 AM'. Stagger cron batch processing with a 100ms delay between API calls.
V0 calendar library Tailwind class conflicts break the calendar UI
Symptom: react-big-calendar and @fullcalendar/react ship their own CSS stylesheets that define grid layout, event colors, and cell heights. Tailwind v4 (V0's default since February 2026) applies a CSS reset that removes margins and sets box-sizing globally. When both are loaded, the calendar cells collapse to 0 height and events overlap in an unreadable stack.
Fix: Import the calendar library's CSS file (import 'react-big-calendar/lib/css/react-big-calendar.css') in the root layout BEFORE any Tailwind imports. For Tailwind v4, wrap all calendar-specific overrides in @layer base { } with explicit !important on height and margin rules, or use the library's CSS custom properties API if available. Always test the calendar render after the first Vercel deploy.
Best practices
Always store scheduled_at in UTC and a separate timezone column with the IANA timezone name — never store a UTC offset integer, which breaks after every DST transition
Set the publish Edge Function to update status to 'publishing' BEFORE the API call, not after — this acts as a distributed lock preventing duplicate publishes when the cron fires twice
Store access_token and refresh_token as encrypted text in the Supabase user_oauth_tokens table using pg_crypto or Supabase Vault — never in localStorage or client-side state
Implement retry logic with a maximum of 3 attempts and exponential backoff (5 min, 15 min, 45 min) — store retry_count and next_retry_at on the post row so the cron skips non-due retries
Show the error_message to users when a post fails with an actionable label — not just 'Failed' but 'Failed: Twitter rate limit reached, retrying at 10:15 AM' or 'Failed: Account disconnected, please reconnect'
Add a status = 'rate_limited' state with retry_after timestamp so users understand rate limit delays are not failures and will self-resolve
Use the Instagram Graph API's content publishing flow (create container, then publish) — a two-step process that AI tools sometimes flatten into one call, causing 'container not ready' errors
When You Need Custom Development
AI tools handle single-user or small-team post scheduling well. Move to custom development when:
- More than 5 social platforms are needed — each platform's OAuth PKCE flow, API client, rate limit handling, and error code mapping is custom work estimated at 2-3 days per platform
- Enterprise team approval workflow: multi-step review before posts publish, with editor/manager/approver roles, audit trails, and notification escalation — well beyond AI tool output
- AI-generated post copy from a content brief: combining Anthropic post generation with scheduled publishing in a single workflow requires custom orchestration and prompt iteration
- White-label scheduling tool offered as a product to other businesses: multi-tenant OAuth token isolation, plan-based platform limits, usage analytics per customer, and billing integration all require custom architecture
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
Which social media platforms can I connect to my app?
The four most commonly supported platforms are Twitter/X (OAuth 2.0 PKCE), LinkedIn (OAuth 2.0 authorization code with Marketing API approval), Facebook Pages (Facebook Graph API with business verification), and Instagram Business accounts (Instagram Graph API via Facebook). TikTok, Pinterest, and YouTube each have their own OAuth implementations and require separate API approvals — budget 2-3 days per additional platform.
How does post scheduling handle different time zones?
Detect the user's timezone automatically from Intl.DateTimeFormat().resolvedOptions().timeZone (e.g., 'America/New_York') and store it in a timezone column alongside the post. Store scheduled_at in UTC using date-fns-tz parseInTimeZone. Display all scheduled times back to the user in their stored timezone using formatInTimeZone. Never store a UTC offset integer — it breaks after Daylight Saving Time transitions.
What happens if a scheduled post fails to publish?
The cron publisher catches the API error, sets status to 'failed', stores the error_message from the platform response, increments retry_count, and reschedules the post 5 minutes later if retry_count < 3. After 3 failed attempts, status stays 'failed' and the user is notified in-app with the error reason. Common failure reasons are rate limits (show retry time), expired OAuth token (show 'reconnect account'), and platform API downtime (show 'platform unavailable, will retry').
Can I schedule the same post to multiple platforms at once?
Yes — the platforms column is a text array (e.g., ['twitter', 'linkedin', 'instagram']). The cron publisher loops over the array and calls each platform's API in sequence within the same post processing job. Platform-specific validation (Instagram requires an image) runs at compose time, not at publish time, so invalid configurations are caught before the post is saved.
How do I store social media OAuth tokens securely?
Store access_token and refresh_token in the user_oauth_tokens Supabase table as encrypted text — use Supabase Vault (SELECT vault.create_secret()) or pg_crypto (pgp_sym_encrypt) for column-level encryption. Set RLS so only the owning user can read their tokens. Never store tokens in localStorage, cookies, or application logs. The service role key used by the Edge Function to read and update tokens must never be exposed to the client.
Do I need a business account to use the LinkedIn or Instagram API?
Yes for posting. LinkedIn requires Marketing Developer Platform access (apply at developer.linkedin.com) which typically takes 1-2 weeks to approve. Instagram only allows posting to Business or Creator accounts via the Instagram Graph API — personal accounts cannot be posted to programmatically. Facebook Page posting requires a verified business Meta app. Twitter/X posting requires a developer account at developer.twitter.com with an approved app.
How do I build a calendar view of scheduled posts?
Use react-big-calendar or @fullcalendar/react. Fetch all scheduled posts for the visible date range from Supabase and pass them as events to the calendar. Color-code by platform. Handle drag-to-reschedule via the onEventDrop callback: update scheduled_at via a Server Action and optimistically update the local event state. Import the calendar library's CSS before Tailwind to prevent style conflicts that collapse event rows.
Can users connect their own social accounts or only a single shared account?
The user_oauth_tokens table stores one token row per (user_id, platform) pair, so every user connects their own individual social accounts. Each user authenticates with their own Twitter/LinkedIn/Instagram credentials through the OAuth flow. If you want a single shared account (e.g., the company's Twitter handle), store the token against a service account user ID and grant other users access via Supabase RLS policies.
Need this feature production-ready?
RapidDev builds social media post scheduling into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.