Add scheduled_posts table for POST /api/posts

Merged
SW
4d ago
+276-0

Adds scheduled_posts, the table behind POST /api/posts (step 2 of recoupable/app#2241: docs āœ… → database → api publish-now → api scheduling). Contract: marketing#154 / docs#375, live at docs.recoupable.dev/api-reference/publishing/create.

Table

One row per post: one video + copy to one connected account on one platform, published now (publish_at NULL) or at publish_at. The row is also the publish workflow's run record, same pattern as music_generations (20260821170000).

Columns follow the contract's Post schema: id, account_id (connection holder, cascade), created_by_account_id (set null), platform, sub_account_id, video_url, cover_url, title, description, privacy, tags text[], publish_at, status, post_url, error_message, text_verified, workflow_run_id, created_at, updated_at (shared trigger_set_updated_at).

Constraints (match the published contract)

RuleCHECK
Platformsinstagram, x, linkedin, youtube (TikTok is a follow-up)
Statusesscheduled, publishing, posted, failed, cancelled
Required copyYouTube needs title; the others need description (blank rejected)
Platform-only fieldssub_account_id LinkedIn only; privacy/tags YouTube only; no cover_url on YouTube (owner rule 2026-10-13)
Lifecyclescheduled needs publish_at; posted needs post_url; failed needs error_message

Length limits (X weighted 280, YT title 100, IG 2,200, LI 3,000) stay in the API's Zod validation; Postgres can't express X's weighting.

Indexes: (account_id, created_at DESC) for List Posts; partial (publish_at) WHERE status = 'scheduled' for sweeping overdue schedules.

Access: RLS on with no policies, anon/authenticated grants revoked, service_role granted CRUD. The API reads and writes as service_role.

Tests

tests/test_scheduled_posts.py on a disposable cluster (no hosted credentials), new CI workflow on PostgreSQL 15 and 16:

  • a publish-now row inserts for each platform with its fields; migration re-applies cleanly
  • missing/blank required copy, unknown platform/status/privacy, platform-only fields on the wrong platform, YouTube cover: each fails on a check constraint (asserted by message)
  • scheduled without publish_at, posted without post_url, failed without error_message rejected; posting updates updated_at
  • account delete cascades; creator delete keeps the post
  • anon and authenticated get permission denied on select/insert/update/delete

Local: 6/6 pass on PostgreSQL 14.

Not applied to any hosted database. Applying it to production is a separate, owner-approved step.

šŸ¤– Generated with Claude Code


Summary by cubic

Adds the scheduled_posts table backing POST /api/posts (step 2 of recoupable/app#2241). One row per post pairs a finished video and its copy with one connected account on Instagram, X, LinkedIn, or YouTube, published immediately or at publish_at; the row doubles as the publish workflow's run record.

Contract and access

  • CHECK constraints enforce the published contract: YouTube needs title (the others need description), sub_account_id is LinkedIn-only and rejects blanks, privacy/tags are YouTube-only, YouTube accepts no cover_url, and each status carries its required field (scheduled needs publish_at, posted needs a non-blank post_url, failed needs error_message).
  • Indexes cover list-by-account queries and sweeping overdue scheduled posts; the migration re-applies cleanly.
  • RLS is on with no policies and anon/authenticated grants are revoked; only service_role reads and writes.
  • Migration details live in tests/SCHEDULED-POSTS.md, linked from the README; tests run on a disposable cluster with CI on PostgreSQL 15 and 16, and the migration is not applied to any hosted database.

Summary by CodeRabbit

  • New Features
    • Added backend support for tracking video posts scheduled for Instagram, X, LinkedIn, and YouTube, including scheduled publish times, publishing status, and results.
    • Platform-specific requirements are enforced for post content and options.
  • Documentation
    • Added guidance on scheduled-post requirements and how to run the related tests.