Developer Notes

Queue & reliability

Target state machine

Every post × account pair is a post_target with its own status, attempts and error. States: pending (draft post) → scheduled → publishing → published or failed. A failed target goes back to scheduled behaviour via next_attempt_at until retries are exhausted; a manual Retry claims it again. The post's own status is recomputed from its targets (published / partially_published / failed / publishing).

Backoff schedule

Retryable failures retry after 1, then 5, then 30 minutes (3 attempts total). Non-retryable errors (e.g. token expired) fail immediately with no next_attempt_at and surface in the Failed tab as "needs attention".

Atomic claiming

The every-minute runner claims due targets with the claim_due_targets SQL function (FOR UPDATE … SKIP LOCKED), so two overlapping runs can never publish the same target twice. Manual retries use the same atomic scheduled/failed → publishing transition (claimTarget).

Stuck-job sweep

Before each run, targets stuck in publishing for more than 10 minutes (crashed worker) are flipped back to failed with an immediate retry, so a crash never strands a post.

Lovable-specific pieces

Scheduling is driven by pg_cron calling a server route every minute, authenticated with the platform-managed LOVABLE_CRON_SECRET. In the native PatientSites build both are replaced by the existing scheduler/queue (e.g. a cron on the PHP stack or a job worker) calling the same claim-and-publish logic; the state machine, backoff and sweep are portable as-is.

Branded Templates

Slot / plate model

A library design is a Template: a swappable background photo plus absolutely positioned layers. Each layer fills a named slot (headline, subhead, accent_fill, accent_text, footer_band, footer_text, logo) and declares a plate — the neutral backing (white, dark, scrim, or the brand band) it sits on. Positions are stored per aspect ratio (square, portrait, story) in percent of canvas width, so one definition drives thumbnails, the live preview and PNG export.

Contrast rules (src/lib/contrast.ts)

  1. Brand-colored text only sits on a neutral plate; if contrast is under 3:1 it is darkened/lightened step by step.
  2. Colored fills pick black or white text, whichever has higher WCAG contrast.
  3. Nothing touches the photo without a backing: plate, brand band, or dark scrim with white text.
  4. The logo always sits on a plate: white variant on dark/colored, dark variant on white.

Derived palette

Computed from the BrandKit, never stored: primary, primaryText, accent (derived if missing), accentText, primaryDarkened, footerBand, footerText, plus the contrast ratios and a list of adjustments (surfaced as the “Adjusted” badge). Neutral mode derives the palette from the template’s original colors instead.

Production

Rendering should move server-side at publish time (headless browser or an SVG-to-PNG service) so output is identical everywhere. The brand kit should be collected once per client during onboarding/design setup. Logo white/dark variants are generated by recoloring the logo’s non-transparent pixels.

Template JSON (Build Up)

{
  "id": "tpl_build_up",
  "name": "Build Up",
  "category": "Movement and Mobility",
  "moods": [
    "bold"
  ],
  "backgroundImageUrl": "https://images.unsplash.com/photo-1476480862126-209bfaa8edc8?w=1200&q=80",
  "aspectRatios": [
    "square",
    "portrait",
    "story"
  ],
  "originalColors": {
    "primary": "#1f5fae",
    "accent": "#f2a33a"
  },
  "layers": [
    {
      "id": "headline",
      "type": "text",
      "slot": "headline",
      "plate": "white",
      "layout": {
        "square": {
          "x": 18,
          "y": 6,
          "w": 64,
          "h": 14,
          "fontSize": 9.5
        },
        "portrait": {
          "x": 18,
          "y": 6,
          "w": 64,
          "h": 14,
          "fontSize": 9.5
        },
        "story": {
          "x": 18,
          "y": 6,
          "w": 64,
          "h": 14,
          "fontSize": 9.5
        }
      },
      "shape": "pill",
      "content": "BUILD UP"
    },
    {
      "id": "circle_0",
      "type": "shape",
      "slot": "accent_fill",
      "plate": "none",
      "layout": {
        "square": {
          "x": 5,
          "y": 34,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "portrait": {
          "x": 5,
          "y": 46.5,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "story": {
          "x": 5,
          "y": 72.89,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        }
      },
      "shape": "circle",
      "content": "10\nMINUTES"
    },
    {
      "id": "week_0",
      "type": "text",
      "slot": "subhead",
      "plate": "dark",
      "layout": {
        "square": {
          "x": 7,
          "y": 58,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "portrait": {
          "x": 7,
          "y": 70.5,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "story": {
          "x": 7,
          "y": 96.89,
          "w": 17,
          "h": 6,
          "fontSize": 3
        }
      },
      "content": "WEEK 1"
    },
    {
      "id": "circle_1",
      "type": "shape",
      "slot": "accent_fill",
      "plate": "none",
      "layout": {
        "square": {
          "x": 28,
          "y": 34,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "portrait": {
          "x": 28,
          "y": 46.5,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "story": {
          "x": 28,
          "y": 72.89,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        }
      },
      "shape": "circle",
      "content": "15\nMINUTES"
    },
    {
      "id": "week_1",
      "type": "text",
      "slot": "subhead",
      "plate": "dark",
      "layout": {
        "square": {
          "x": 30,
          "y": 58,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "portrait": {
          "x": 30,
          "y": 70.5,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "story": {
          "x": 30,
          "y": 96.89,
          "w": 17,
          "h": 6,
          "fontSize": 3
        }
      },
      "content": "WEEK 2"
    },
    {
      "id": "circle_2",
      "type": "shape",
      "slot": "accent_fill",
      "plate": "none",
      "layout": {
        "square": {
          "x": 51,
          "y": 34,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "portrait": {
          "x": 51,
          "y": 46.5,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "story": {
          "x": 51,
          "y": 72.89,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        }
      },
      "shape": "circle",
      "content": "20\nMINUTES"
    },
    {
      "id": "week_2",
      "type": "text",
      "slot": "subhead",
      "plate": "dark",
      "layout": {
        "square": {
          "x": 53,
          "y": 58,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "portrait": {
          "x": 53,
          "y": 70.5,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "story": {
          "x": 53,
          "y": 96.89,
          "w": 17,
          "h": 6,
          "fontSize": 3
        }
      },
      "content": "WEEK 3"
    },
    {
      "id": "circle_3",
      "type": "shape",
      "slot": "accent_fill",
      "plate": "none",
      "layout": {
        "square": {
          "x": 74,
          "y": 34,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "portrait": {
          "x": 74,
          "y": 46.5,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        },
        "story": {
          "x": 74,
          "y": 72.89,
          "w": 21,
          "h": 21,
          "fontSize": 3.6
        }
      },
      "shape": "circle",
      "content": "25\nMINUTES"
    },
    {
      "id": "week_3",
      "type": "text",
      "slot": "subhead",
      "plate": "dark",
      "layout": {
        "square": {
          "x": 76,
          "y": 58,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "portrait": {
          "x": 76,
          "y": 70.5,
          "w": 17,
          "h": 6,
          "fontSize": 3
        },
        "story": {
          "x": 76,
          "y": 96.89,
          "w": 17,
          "h": 6,
          "fontSize": 3
        }
      },
      "content": "WEEK 4"
    },
    {
      "id": "band",
      "type": "shape",
      "slot": "footer_band",
      "plate": "none",
      "layout": {
        "square": {
          "x": 0,
          "y": 81,
          "w": 100,
          "h": 19,
          "fontSize": 8,
          "align": "left"
        },
        "portrait": {
          "x": 0,
          "y": 106,
          "w": 100,
          "h": 19,
          "fontSize": 8,
          "align": "left"
        },
        "story": {
          "x": 0,
          "y": 158.78,
          "w": 100,
          "h": 19,
          "fontSize": 8,
          "align": "left"
        }
      },
      "shape": "band",
      "content": "GRADUALLY"
    }
  ],
  "logoSlots": [
    {
      "id": "band_right",
      "label": "Bottom right (on band)",
      "plate": "band",
      "layout": {
        "square": {
          "x": 70,
          "y": 84,
          "w": 26,
          "h": 13
        },
        "portrait": {
          "x": 70,
          "y": 109,
          "w": 26,
          "h": 13
        },
        "story": {
          "x": 70,
          "y": 161.78,
          "w": 26,
          "h": 13
        }
      }
    },
    {
      "id": "top_right",
      "label": "Top right (white plate)",
      "plate": "white",
      "layout": {
        "square": {
          "x": 80,
          "y": 23,
          "w": 16,
          "h": 8
        },
        "portrait": {
          "x": 80,
          "y": 23,
          "w": 16,
          "h": 8
        },
        "story": {
          "x": 80,
          "y": 23,
          "w": 16,
          "h": 8
        }
      }
    }
  ]
}