Add scheduled_posts table for POST /api/posts
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)
| Rule | CHECK |
|---|---|
| Platforms | instagram, x, linkedin, youtube (TikTok is a follow-up) |
| Statuses | scheduled, publishing, posted, failed, cancelled |
| Required copy | YouTube needs title; the others need description (blank rejected) |
| Platform-only fields | sub_account_id LinkedIn only; privacy/tags YouTube only; no cover_url on YouTube (owner rule 2026-10-13) |
| Lifecycle | scheduled 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)
scheduledwithoutpublish_at,postedwithoutpost_url,failedwithouterror_messagerejected; posting updatesupdated_at- account delete cascades; creator delete keeps the post
anonandauthenticatedgetpermission deniedon 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 needdescription),sub_account_idis LinkedIn-only and rejects blanks,privacy/tagsare YouTube-only, YouTube accepts nocover_url, and each status carries its required field (scheduledneedspublish_at,postedneeds a non-blankpost_url,failedneedserror_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/authenticatedgrants are revoked; onlyservice_rolereads 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.