Files
muninn/docs/superpowers/specs/2026-07-18-posts-design.md
2026-07-18 16:43:09 +03:00

5.4 KiB

Scheduled Posts — Design

Purpose

Extends the existing Sites & Social Accounts Manager with a Posts resource: a content calendar that an AI agent can use to draft content, schedule it against a specific site (and optionally a specific social account), and track its publishing lifecycle. This app is the data store and scheduling calendar only — it does not publish anything itself. The AI is responsible for reading due posts, publishing them externally (to Twitter, a blog, etc.), and reporting the result back via the API.

Scope

  • CRUD UI for Posts (Livewire, consistent with the existing Sites/Social Accounts pages)
  • Full read/write JSON API for Posts (unlike the read-only Sites/Accounts API, since the AI drives the whole post lifecycle: draft → schedule → publish/fail)
  • A post belongs to exactly one Site (required) and optionally one Social Account
  • No authentication anywhere (UI or API) — local network only, consistent with the rest of the app
  • No background scheduler or per-platform publishing integration — publishing is entirely the AI's responsibility; this app only tracks what's due and what happened

Out of scope:

  • Automatic/scheduled publishing (no queue job that calls out to Twitter/blog APIs)
  • Enforced consistency between a post's type and its linked account's network — these are independent fields by design
  • Edit history / versioning of post content
  • Authentication/authorization (UI or API)

Data Model

posts table

Column Type Constraints
id bigint PK
site_id bigint required, FK → sites.id
social_account_id bigint nullable, FK → social_accounts.id
type string (enum) required — one of: twitter, instagram, facebook, reddit, tiktok, linkedin, youtube, blog, other (same network vocabulary as SocialAccount::NETWORKS, plus blog)
title string nullable
body text required
status string (enum) required, default draft — one of: draft, scheduled, published, failed
scheduled_at timestamp nullable
created_at / updated_at timestamps

Relationships:

  • Post belongsTo Site (required)
  • Post belongsTo SocialAccount (nullable)
  • No reverse enforcement — a Site or SocialAccount can be deleted independent of posts referencing it unless a DB-level constraint is added (see Testing/Edge Cases below for the chosen behavior).

No validation ties type to the linked account's network — a post can be type=twitter with no account linked, or linked to an Instagram account with type=blog, etc. This is intentional.

UI (Livewire, no auth)

New top-level nav item "Posts", alongside the existing Sites and Social Accounts links.

  • Table view: site name, type, title (or a truncated body preview if no title), status, scheduled_at, linked account (or "—"), actions (edit/delete)
  • Create/Edit modal form:
    • Site — dropdown (required), populated from all Sites
    • Social Account — dropdown (optional, "None" default), populated from all Social Accounts
    • Type — dropdown (required): twitter, instagram, facebook, reddit, tiktok, linkedin, youtube, blog, other
    • Title — text input (optional)
    • Body — textarea (required)
    • Status — dropdown (required): draft, scheduled, published, failed (default draft)
    • Scheduled At — datetime input (optional)
  • Delete with confirmation

API (full CRUD, no auth, JSON)

Standard Laravel API resource controller with all five REST actions (this resource is read+write, unlike Sites/Accounts).

  • GET /api/posts — list all posts
    • ?status= — optional exact match filter
    • ?site_id= — optional exact match filter
    • ?social_account_id= — optional exact match filter
    • ?due=1 — optional: filters to posts where status=scheduled AND scheduled_at <= now() — the "what needs publishing right now" query the AI polls
  • GET /api/posts/{id} — single post
  • POST /api/posts — create a post (site_id and body required at minimum; type required; other fields optional per the table above)
  • PUT /api/posts/{id} / PATCH /api/posts/{id} — update a post (e.g. AI sets status to published or failed after attempting to publish externally, or edits content/schedule)
  • DELETE /api/posts/{id} — delete a post

JSON response shape: {id, site_id, social_account_id, type, title, body, status, scheduled_at, created_at, updated_at}.

Validation for create/update: site_id required and must reference an existing site; social_account_id nullable, must reference an existing account when present; type required, must be one of the enum values; body required; title nullable; status required, must be one of the enum values, defaults to draft if omitted on create; scheduled_at nullable, valid datetime when present.

Testing / Edge Cases

  • Feature tests for CRUD flows (Livewire component tests), mirroring the existing Sites/SocialAccounts test structure
  • Feature tests for the API: list, filter (status/site_id/social_account_id/due), show, create, update (including status transitions), delete, validation failures, 404s
  • Deleting a Site or Social Account that has posts referencing it: use nullOnDelete() for social_account_id (post keeps existing, just loses its account link) and cascadeOnDelete() for site_id (a post cannot exist without its site, so deleting the site deletes its posts) — this keeps the data model consistent without requiring UI-level guardrails against deletion.