Research-based methodology. This guide draws on the X (formerly Twitter), LinkedIn, and Threads developer docs, Vercel and Railway scheduling docs, and our own builds with Claude. First-person testing is called out where it exists. How we research.

Why content calendars are a good 2026 niche

Social scheduling is one of those categories that looks “done” from the outside but keeps creating new winners. Buffer is the granddaddy. Hypefury became a serious business by being opinionated about X. Typefully built an audience by being design-first about a single platform. Each found a wedge by either niching down (one platform, deep) or specializing the audience (creators, not agencies; threads, not posts).

For a solo founder in 2026, the opportunity is the same wedge play: pick one or two platforms and one persona, and build a scheduler that knows their habits better than the generalists do. A content calendar for solo creators on X and LinkedIn is a real product. A “post anywhere” tool that competes with Buffer head-on is not.

This guide assumes you’ve picked your wedge and are ready to build. If you’ve read the broader build-a-SaaS-with-Claude playbook, this is the scheduling-shaped extension of it.

What makes scheduling SaaS unforgiving

The product looks deceptively simple: store a post, store a time, post it at that time. The places it gets hard:

Each platform’s API is a different world

X uses OAuth 2.0 with PKCE and has separate “v2” endpoints with their own rate limits. LinkedIn requires Marketing Developer Platform access for some endpoints and uses share-on-LinkedIn for personal posts. Threads, Bluesky, Mastodon, and Instagram each have their own auth flow, scope model, and content rules. There is no shortcut — each integration is its own project.

Scheduling reliability is a customer-loss event

If a habit-tracker reminder is late, no one cares. If a content calendar misses a 9am Tuesday post, the customer churns within the week. “The whole reason I pay for this” is the line you’ll read in your support inbox. Reliability requires queueing, idempotency, retries, and observability that most v1 builds skip.

Drafts and scheduled state are easy to bungle

A post can be a draft, then scheduled, then attempted, then succeeded or failed, then maybe rescheduled, then cancelled. Modeling this as a flat status column on a flat table leads to inconsistent state. Modeling it cleanly takes care up front.

Timezones again

The user creates a post for “Tuesday at 9am.” The user is in Berlin. They travel to New York. Should the post fire at 9am Berlin or 9am New York? Pick one, document it, build for it. The unforgivable answer is “sometimes one, sometimes the other.”

Step 1 — Data model with platform-specific JSON

The data shape: a workspace owns N social_accounts (one per platform per account), and a scheduled_post targets one or more social_accounts at one scheduled_for time. The platform-specific payload (text, media URLs, thread structure, LinkedIn article fields) lives in JSONB so each platform can have its own shape without a migration every time you add a new platform.

Prompt 1 — Data model + RLS
I'm building a content scheduling SaaS for solo creators and small
teams. Design the Postgres schema for Supabase.

Tables:

- workspaces (id, name, owner_user_id, plan, created_at)
- workspace_members (workspace_id, user_id, role: 'owner'|'editor'|'viewer',
  primary key (workspace_id, user_id))
- social_accounts (id, workspace_id, provider: 'x'|'linkedin'|'threads',
  display_name, handle, avatar_url, encrypted_access_token,
  encrypted_refresh_token, expires_at, last_verified_at, is_active,
  created_at)
- scheduled_posts (id, workspace_id, author_user_id, status:
  'draft'|'scheduled'|'publishing'|'published'|'failed'|'cancelled',
  scheduled_for timestamptz, scheduled_for_tz text, content jsonb,
  created_at, updated_at)
  -- content jsonb is platform-agnostic at this layer
- post_targets (post_id, social_account_id, payload jsonb,
  primary key (post_id, social_account_id))
  -- payload is the PLATFORM-SPECIFIC shape: for x, { text, media[],
  -- reply_to_id }; for linkedin, { commentary, article_url, visibility }
- publish_attempts (id, post_target_id, attempt_number, status:
  'success'|'rate_limited'|'auth_failed'|'platform_error'|'network_error',
  platform_post_id text, platform_response jsonb, started_at,
  finished_at, error text)

Constraints:
- A scheduled_post in status='scheduled' must have scheduled_for in the
  future at insert time
- post_targets cannot exist without at least the post in draft state
- publish_attempts is append-only

Generate:
1. Full SQL with indexes (workspaces.owner_user_id;
   scheduled_posts(workspace_id, scheduled_for) for the calendar query;
   scheduled_posts(status, scheduled_for) for the worker query)
2. RLS: workspace members can read/write their workspace's posts;
   role='viewer' is read-only; only role='owner' can manage
   social_accounts; service role only can write publish_attempts
3. A status state machine doc as SQL comments: which transitions are
   legal, which are not
4. Encrypted token columns using pgcrypto pgp_sym_encrypt

The state machine matters. Without it you’ll eventually have a post in publishing state that the worker has crashed on, and your worker will pick it up again and double-post. Be explicit about what transitions are legal.

Step 2 — OAuth into X and LinkedIn

Start with X and LinkedIn. They’re the two platforms where solo creators actually want to schedule. Threads and Bluesky are nice-to-have second-tier integrations.

Prompt 2 — OAuth connect flow (X + LinkedIn)
Build the OAuth connect flow for X and LinkedIn in a Next.js App Router
app.

For X (Twitter):
- OAuth 2.0 with PKCE
- Scopes: tweet.read, tweet.write, users.read, offline.access
- Authorize URL: https://twitter.com/i/oauth2/authorize
- Token URL: https://api.twitter.com/2/oauth2/token
- Store: access_token, refresh_token, expires_in, scope, the user's
  numeric id and handle

For LinkedIn:
- OAuth 2.0 (not OIDC for posting permissions)
- Scopes: w_member_social, r_liteprofile
- Authorize URL: https://www.linkedin.com/oauth/v2/authorization
- Token URL: https://www.linkedin.com/oauth/v2/accessToken
- Store: access_token, expires_in, the member URN

Build:
1. /api/connect/[provider] route that:
   - Generates a CSRF state JWT bound to (workspace_id, provider, user_id)
   - For X: generates a PKCE code_verifier and stores its hash in the
     state JWT
   - Redirects to the provider authorize URL with the right scopes and
     redirect_uri
2. /api/connect/[provider]/callback that:
   - Verifies state, exchanges code for tokens
   - Calls a "whoami" endpoint to get display_name + handle/avatar
   - Encrypts tokens with pgcrypto, upserts a social_accounts row
   - Redirects to /accounts with success
3. A token-refresh helper called before any worker publish:
   - For X: POST to token URL with grant_type=refresh_token
   - For LinkedIn: tokens last 60 days, no refresh; instead detect 401
     and mark sync_status='reauth_required'

Reference the official X v2 OAuth docs and LinkedIn share-on-LinkedIn
docs. Use the latest API version.

Three details that bite: X requires the redirect URI to be HTTPS even in dev (use a tunnel like ngrok), LinkedIn doesn’t do refresh tokens for member auth so you must handle reauth gracefully, and both providers will reject your scopes if you haven’t configured them in the developer portal first. Set up the portal config before you write code.

Step 3 — The scheduled-publish worker

This is the core of the product. The worker has three jobs: find posts whose scheduled_for has passed, atomically claim them so no other worker picks the same one, and call the right platform API.

Two architecture options. Vercel Cron + a function-based worker works for low volume and is the cheapest. A dedicated Railway worker is more reliable past a few hundred posts/day. The Vercel vs Railway comparison covers the tradeoff. For a v1 launch, start with Vercel Cron and migrate when you actually need to.

Prompt 3 — Scheduled-publish worker
Build the scheduled-publish worker for a content calendar SaaS, designed
to run on Vercel Cron at every minute (* * * * *).

Endpoint: /api/cron/publish

Behavior:

1. Authorization: require the CRON_SECRET header (Vercel Cron sends this
   automatically). Reject otherwise.

2. Claim work atomically:
   UPDATE scheduled_posts
   SET status = 'publishing', updated_at = now()
   WHERE id IN (
     SELECT id FROM scheduled_posts
     WHERE status = 'scheduled' AND scheduled_for <= now()
     ORDER BY scheduled_for ASC
     LIMIT 25
     FOR UPDATE SKIP LOCKED
   )
   RETURNING *;
   -- This is the critical pattern: SKIP LOCKED prevents two cron runs
   -- from grabbing the same post.

3. For each claimed post, load its post_targets and the
   social_accounts. For each target:
   a) Refresh the OAuth token if expired (X only; LinkedIn surfaces
      reauth via 401 below)
   b) Call the platform API:
      - X: POST https://api.twitter.com/2/tweets with payload.text,
        payload.media.media_ids if any
      - LinkedIn: POST https://api.linkedin.com/v2/ugcPosts using the
        member URN
   c) Insert a publish_attempts row with the result
   d) On success: store platform_post_id, mark target done
   e) On 4xx auth error: mark social_account.is_active=false, notify
      the workspace owner via email
   f) On 4xx rate limit: do NOT mark failed; reschedule scheduled_for
      to now()+5min and set status back to 'scheduled'
   g) On 5xx / network: increment retry counter, exponential backoff

4. Rollup: if all targets succeeded, set scheduled_post.status='published'.
   If any target failed permanently, set status='failed' and email the
   author.

5. Idempotency: if a target already has a successful publish_attempt,
   skip it (defense against re-claiming a post mid-flight).

6. Observability: emit structured logs with post_id, target_id, status.

Output: the route handler in TypeScript using the Supabase service-role
client. Reference the latest X v2 and LinkedIn UGC Posts API docs.

FOR UPDATE SKIP LOCKED is the line that makes this design work. Without it, two concurrent cron invocations grab the same row and post twice. With it, exactly one wins.

Step 4 — The weekly calendar UI

Customers want to see their week at a glance. The classic layout is a 7-column grid (one column per day) with cards for each scheduled post stacked by time. Drag a card to a different day to reschedule. Click a card to open the editor.

Prompt 4 — Weekly calendar UI
Build the weekly content calendar at /calendar for a content scheduling
SaaS. Use Next.js, Tailwind, and dnd-kit for drag-and-drop.

Layout:

- Top bar: week selector (« previous, current week label, next »),
  workspace timezone label, "+ New post" button
- Below: a 7-column grid (Mon–Sun in the workspace's timezone)
- Each column has a header (day name + date) and a vertical list of
  post cards sorted by scheduled_for time

Each post card shows:
- A small icon strip for each target platform (X, LinkedIn, etc.)
- The first 80 chars of the post text
- The scheduled time in 12h or 24h depending on workspace setting
- A status indicator: blue for scheduled, gray for draft, green for
  published, red for failed

Drag-and-drop:
- Use dnd-kit's DndContext + droppable per day column
- Dropping a card on a different day updates scheduled_for to the SAME
  time-of-day on the new date (don't reset the time)
- Show a subtle confirmation toast: "Rescheduled to Tue 9:00am"
- Optimistic UI: move the card immediately, server action persists,
  rollback on error

Server action: reschedule(post_id, new_scheduled_for_iso) that:
- Verifies new_scheduled_for is in the future
- Verifies post.status is 'scheduled' or 'draft'
- Updates scheduled_for and revalidates /calendar

Click on an empty cell: opens the post composer prefilled with that
day+time.

Click on a post card: opens the post composer with that post loaded.

Mobile: collapse to a single-column day-at-a-time view with a horizontal
swipeable date picker.

Reference dnd-kit docs for sortable + droppable patterns.

The drag-to-reschedule interaction is the moment the customer feels the product. Spend an extra hour making the drop animation crisp. It’s the difference between “feels professional” and “feels janky.”

Step 5 — Retries, failures, and notifying the user

Failure is normal. APIs go down, tokens expire, content gets rejected for policy reasons. The product’s job is not to never fail — it’s to fail loudly enough that the user can intervene before it matters.

Prompt 5 — Retry logic + failure notifications
Build the retry-and-notify system for a content scheduling SaaS.

Retry rules per failure type:

- network_error / 5xx: retry up to 3 times with exponential backoff
  (30s, 2min, 10min). After 3 attempts, mark target failed.
- rate_limited: re-schedule the post to scheduled_for + 5min. Don't count
  against the retry budget.
- auth_failed (token revoked / 401 on a known-good account):
  - Mark the social_account.is_active = false
  - Mark the post target as auth_blocked (a new state) -- do NOT mark
    the parent post failed unless ALL targets are blocked
  - Send an immediate email to workspace owner: "[Account] disconnected.
    Reconnect to publish queued posts."
- platform_error (4xx not auth or rate limit, e.g., content rejected):
  - Mark target failed
  - Surface the platform's error message verbatim to the user

Notifications:

1. On any post that ends in status='failed':
   - Send a Resend email to the post author within 60 seconds
   - Subject: "Post didn't publish: [first 40 chars]"
   - Body: which platform failed, what the error said, a button "Edit
     and retry" linking to /posts/[id]
2. On any social_account being marked is_active=false:
   - Send a Resend email to workspace owner immediately
   - Subject: "[Account] disconnected from [Platform]"
   - Body: "We couldn't post to [account] because the connection
     expired. Reconnect to keep your queue running."
3. Daily digest at 8am workspace local time if there are any failed
   posts in the last 24h that haven't been re-attempted by the user.

Build:
- The retry orchestration as part of the worker in step 3
- A `notifications` table for audit + dedupe
- The Resend email templates using react-email
- A /posts/failed page listing failures with retry buttons

Reference the official Resend docs for batching + react-email templates.

The single most useful UI affordance: a banner at the top of the dashboard whenever any social_account is disconnected. Customers don’t check email reliably; they do see the banner the next time they log in.

Pricing for a small-team scheduler

Pricing models that work for content schedulers in 2026:

  • Solo — $15/mo — 1 user, 3 social accounts, unlimited scheduled posts.
  • Team — $49/mo — up to 5 users, 10 social accounts, approval workflow, post analytics.
  • Agency — $149/mo — up to 20 users, 30 social accounts, client workspaces, white-label option.

Avoid per-account pricing for the bottom tier — solo creators want to connect their X and LinkedIn without doing arithmetic. Charge for users and meaningful caps, not for accounts.

If you have a content angle as well (not just scheduling), bundle a newsletter via Beehiiv or Substack — the Beehiiv vs Substack comparison covers which tool fits which audience.

How to differentiate from Buffer/Hypefury

Three differentiation angles working in 2026:

  • One platform, deeply — instead of supporting six platforms shallowly, build the best X scheduler or the best LinkedIn scheduler. Hypefury did this for X and built a real business.
  • One niche, deeply — a scheduler for indie hackers, a scheduler for solo lawyers, a scheduler for fitness creators. The product looks similar; the templates, integrations, and copy are entirely different.
  • One workflow, fully — tie scheduling to ideation (AI-assisted draft generation), to analytics (which posts performed), and to recurring queues (evergreen content) so the customer never leaves your tool to manage their content.

For more 2026 SaaS ideas in this space, our ideas page covers a few content-tooling angles in more depth. The wedge is always “what does Buffer not do well for THIS audience.”

Content calendar SaaS, in one paragraph
Pick one platform deep or one niche deep. Reliability is the product. Notify loudly on failure.

Generic Buffer competitors die. Schedulers that own one platform or one audience win. The technical bar is reliability — queueing, idempotency, retries, observability — not features. Ship the boring infrastructure first, then the polish.

Related guides

Get one SaaS build breakdown every week

The stack, prompts, pricing, and mistakes to avoid — for solo founders building with AI.