Vibecode FeedHive
track this build4 phases, 12 steps, beginner friendly0%The visible AI social scheduling loop is buildable, but a credible replacement needs more than the first screen. FeedHive earns its keep through platform APIs, distribution, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of FeedHive. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # FeedHive · indie build A scheduler for one author whose hard problems are that every network has a different and changing set of caps and formats, that a worker publishing a due post must do it exactly once even when the API times out, and that analytics history cannot be recovered after the fact so you either record it from day one or you do not have it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, a small server-rendered editor and one worker process | scheduling is a worker problem and the editor is a thin skin over a queue | | Storage | PostgreSQL for rows, media files on disk beside it, checksummed | a post is a row plus files and the files are the part that goes wrong | | Publishing | Official platform APIs only, one adapter per network | anything that drives a logged-in browser session breaks the terms and loses the account | | Scheduling | Due rows claimed by conditional update, with a per-attempt request identifier | two workers and one due post is a double publish that you cannot delete quietly | | Limits | A limits table you fill in from the documentation, with the date you read it | the caps are published, they change, and guessing them costs you a suspended app | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the worker. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Holds posts, schedules and the append-only metric history, which grows and needs real indexes. - Get it: Your package manager or a small managed instance. - Verify: Migrations apply and the worker claims a row - [ ] **A developer application per network you intend to publish to** · varies sharply by network; check each current published pricing page before you commit - Why: Each network has its own review, its own scopes and its own publishing caps, and at least one of them will refuse you. - Get it: Register an application on each network's developer portal and request the publishing permission. - Verify: A test post reaches each connected account through the API - [ ] **ffmpeg** (optional) · free - Why: Per-network renditions are the only way to stop an upload being rejected for a format you did not check. - Get it: Your package manager. - Verify: ffmpeg -version prints a version - [ ] **The limits table, filled in from current documentation** · free - Why: Every refusal in this build cites a number from this table, and a table filled in from memory produces refusals that are wrong in both directions. - Get it: Read each network's current publishing documentation and write the caps, windows and allowed types down with today's date. - Verify: Every row has a checked_on date and the editor shows it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. - analytics history and attribution - team approvals and asset workflows - protection from API and policy changes - calendar-provider edge cases and timezone correctness - reliable direct publishing across networks If one of those is essential to you, that is the reason to keep paying for FeedHive, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · FeedHive The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal posting queue that drafts per network, validates against each network's published limits, and publishes only through documented APIs to replace FeedHive. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node 22, a small server-rendered editor and one worker process · scheduling is a worker problem and the editor is a thin skin over a queue - Storage: PostgreSQL for rows, media files on disk beside it, checksummed · a post is a row plus files and the files are the part that goes wrong - Publishing: Official platform APIs only, one adapter per network · anything that drives a logged-in browser session breaks the terms and loses the account - Scheduling: Due rows claimed by conditional update, with a per-attempt request identifier · two workers and one due post is a double publish that you cannot delete quietly - Limits: A limits table you fill in from the documentation, with the date you read it · the caps are published, they change, and guessing them costs you a suspended app ### Data model (create this before Phase 1) - posts(id, title, body_master, created_at, archived_at) · the thing you wrote once - variants(id, post_id, network, body, media_ids_json, validated_at, validation_errors_json) · one row per network, because the caps and the allowed formats differ - schedules(id, variant_id, due_at_utc, author_timezone, state, claimed_by, claimed_at, attempts) · times stored in UTC with the authoring zone kept beside them - publish_attempts(id, schedule_id, attempt, request_id, http_status, remote_id, error_code, at) · every attempt including the ones that timed out - media(id, path, bytes, mime, width, height, duration_ms, checksum) · checksummed so one file is uploaded once per network and reused - network_limits(network, max_chars, max_media, allowed_mime_json, posts_per_window, window_seconds, container_ttl_seconds, checked_on) · maintained by hand, with the date you last read the documentation - quota_usage(network, window_start, count) · what you have spent against the published cap in the current moving window - metrics(remote_id, network, collected_at, metric, value) · collected on a schedule, because nobody will give you last month's numbers later ### Phase 1 · Variants that are checked before they are scheduled Build: Fill network_limits by reading each platform's current publishing documentation and record the date you read it. Validate every variant against its row: character count by the network's own counting rule, media count, and allowed media types. Show failures in the editor as specific refusals rather than a generic error, and refuse to schedule a variant that has never validated. Trap: Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. Done when: A variant exceeding its network's recorded character limit cannot be scheduled and names the limit and the date it was checked, tested. Character counting is per adapter and each adapter is tested against a fixture containing emoji, combining marks and a long URL. A media type not in the network's allowed list is refused at validation rather than at publish, tested with a PNG against an adapter that accepts only JPEG. Every network_limits row carries a checked_on date and the editor displays it. Do not build yet: No scheduling, no publishing, no media pipeline yet. ### Phase 2 · A worker that publishes exactly once Build: Claim a due schedule with a conditional update that sets state and claimed_by only if the row is still pending, and take nothing that another worker already holds. Generate a request identifier per attempt and store it before the call. On a timeout, mark the attempt unknown and reconcile by looking for the post on the network before ever trying again. Trap: Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. Done when: Two workers started against the same due row result in one publish, tested by running both against a stubbed adapter with a delay. A publish that times out leaves the attempt in an unknown state and does not retry until reconciliation has run, tested. Reconciliation finds an existing remote post and records its identifier instead of publishing again, tested with a stub that reports the post exists. A crashed worker's claim expires after a stated interval and the row becomes claimable again, tested by killing the process. Do not build yet: No quota accounting, no media upload, no metrics yet. ### Phase 3 · Living inside the published caps Build: For each network record the published publishing cap and its window. One that matters concretely: the Instagram content publishing documentation states a limit of 100 API-published posts within a 24-hour moving period, that a carousel of up to ten items counts as a single post, and that a created container expires if it is not published within 24 hours. Track usage in a moving window and hold posts back rather than letting them fail. Trap: Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. Done when: Usage is counted in a sliding window of the documented length and not per calendar day, tested by placing attempts either side of a midnight boundary. A post that would exceed the recorded cap is deferred with a reason rather than attempted, tested. A media container older than the recorded container lifetime is discarded and rebuilt rather than published into, tested with a stubbed clock. Every cap in the code reads from network_limits and no number is hard-coded in an adapter, verified by review. Do not build yet: No metrics collection, no export, no recycling yet. ### Phase 4 · Media that survives the upload Build: Checksum every file on import. Derive per-network renditions with recorded parameters and cache them by checksum and network, so the same image is prepared once. Upload through each network's documented media flow and store the remote media identifier against the rendition. Keep the original untouched. Trap: Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. Done when: The same source file scheduled to two networks produces two renditions and one stored original, tested. A retried publish reuses the cached rendition and the existing remote media identifier rather than uploading again, tested. A source whose type is not accepted by the target network is converted with recorded parameters or refused, never uploaded unchanged and hoped for, tested with a PNG against a JPEG-only adapter. Deleting a post leaves the original file until an explicit purge, tested. Do not build yet: No metrics, no export, no operations work yet. ### Phase 5 · Numbers you can only collect forwards, and a way out Build: Collect the metrics each network exposes for your own posts on a schedule and append them with the time they were collected. Never overwrite an earlier reading. Export posts, variants, schedules, attempts and metrics as newline-delimited JSON with the media manifest. When a token expires, mark that network disconnected, hold its queue, and keep every other network publishing. Trap: Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist. Done when: Metrics are appended with a collection time and no row is ever updated, asserted in the suite. An expired token marks one network disconnected, holds its queue and leaves other networks publishing, tested. Export produces every table plus a media manifest and reimports into an empty database with identical counts, tested. A post that was published is never deleted locally by a failed refresh, only marked, tested by simulating an API returning a not-found. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that drives a logged-in browser session or an undocumented endpoint, because it breaks the platform terms and the account it burns is yours. - Automatic engagement, follow loops and comment generation, which are the behaviours every network's spam policy is written about. - Team approval workflows and asset libraries, which are a permissions model and a storage product rather than a phase here. - Backfilling analytics from before you started collecting, because the networks do not expose it and inventing it would be a lie in a chart. ### README must contain - That publishing goes through documented APIs only, and what happens to an account that uses anything else - The network_limits table, where the numbers came from, and the instruction to recheck them on a stated cadence - That metrics are only ever collected forwards, so history begins the day you start and cannot be recovered - That a due post is claimed by conditional update and that a timed-out publish is reconciled rather than retried - Where media originals live, how renditions are cached, and what a purge deletes ===== AGENTS.md ===== # Agent instructions · FeedHive indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, a small server-rendered editor and one worker process, PostgreSQL for rows, media files on disk beside it, checksummed, Official platform APIs only, one adapter per network, Due rows claimed by conditional update, with a per-attempt request identifier, A limits table you fill in from the documentation, with the date you read it. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. ===== BUILD_PLAN.md ===== # Build plan · FeedHive A scheduler for one author whose hard problems are that every network has a different and changing set of caps and formats, that a worker publishing a due post must do it exactly once even when the API times out, and that analytics history cannot be recovered after the fact so you either record it from day one or you do not have it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Variants that are checked before they are scheduled A variant cannot enter the queue until it passes the limits recorded for its network. ### Steps 1. Write the limits table by hand from the documentation With the date, because these numbers move and stale ones fail at publish time. Files: `migrations/002_network_limits.sql`, `docs/limits.md` 2. Implement a counting rule per adapter String length is not what any of these networks measure. Files: `src/adapters/*/count.ts`, `tests/counting.test.ts` 3. Refuse to schedule an unvalidated variant Validation at publish time is validation that happens while you are asleep. Files: `src/schedule/create.ts`, `tests/validate-gate.test.ts` ### Done when - [ ] An over-length variant cannot be scheduled - [ ] Counting is per adapter and tested with awkward strings - [ ] A disallowed media type is refused at validation - [ ] Every limits row shows when it was last checked ### Watch out - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. ## Phase 2 · A worker that publishes exactly once A due post is published once even when two workers run and the API times out. ### Steps 1. Claim with a conditional update, not a select The row count from the update is the lock. Files: `src/worker/claim.ts`, `tests/double-claim.test.ts` 2. Record the request identifier before the call So an unknown outcome is investigable rather than guessed at. Files: `src/worker/publish.ts` 3. Reconcile unknowns before retrying Look for the post before you make a second one. Files: `src/worker/reconcile.ts`, `tests/unknown-publish.test.ts` ### Done when - [ ] Two workers publish a due post once - [ ] A timeout leaves an unknown, not a failure - [ ] Reconciliation adopts an existing remote post - [ ] An expired claim becomes claimable again ### Watch out - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. ## Phase 3 · Living inside the published caps The queue spends against each network's published cap deliberately rather than discovering it through errors. ### Steps 1. Count usage in a sliding window The documented windows are moving, not calendar days. Files: `src/limits/window.ts`, `tests/sliding-window.test.ts` 2. Defer rather than fail when the cap is reached A deferred post is recoverable; a rejected one needs a human. Files: `src/worker/claim.ts` 3. Expire containers before you publish into them A container has a published lifetime and it is shorter than a weekend. Files: `src/adapters/instagram/container.ts` ### Done when - [ ] Usage is counted in a sliding window - [ ] A capped post is deferred with a reason - [ ] An expired container is rebuilt, not used - [ ] No cap is hard-coded outside the limits table ### Watch out - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. ## Phase 4 · Media that survives the upload A file is prepared once per network, uploaded once, and never silently re-encoded into something the network rejects. ### Steps 1. Checksum on import and key everything by it The checksum is the identity of the file, not the filename. Files: `src/media/import.ts`, `migrations/003_media.sql` 2. Cache renditions by checksum and network Prepared once, uploaded once, reused on retry. Files: `src/media/render.ts`, `tests/rendition-cache.test.ts` 3. Keep the original and purge only on request Re-encoding from a rendition is how quality quietly dies. Files: `src/media/purge.ts` ### Done when - [ ] One source yields per-network renditions and one original - [ ] A retry reuses the rendition and media identifier - [ ] An unsupported type is converted or refused - [ ] Originals survive post deletion until purged ### Watch out - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. ## Not in this build - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. ## After v1, if you want it - Recycling, implemented as an evergreen post returning to the queue as a draft that you approve again rather than as an automatic repost - A limits drift checker that flags any network_limits row older than your chosen cadence - A per-network dry run that renders exactly what would be sent, which catches format mistakes before they cost a cap slot ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Posts, schedules, attempts and metrics. DATABASE_URL=postgres://feedhive@localhost/feedhive # Required. Originals and cached renditions, keyed by checksum. MEDIA_DIR=./data/media # Required. How long a claim survives a dead worker before the row is claimable again. WORKER_CLAIM_TTL_SECONDS=300 # Optional · secret. Drafting per-network variants. The app works without it and you write the variants yourself. LLM_API_KEY=sk-...redacted # Optional. Drafting stops at this figure for the month and manual editing continues. LLM_MONTHLY_CEILING=3.00 # Required · secret. Per-network OAuth tokens and refresh state. Never in the database dump you share. NETWORK_TOKENS_PATH=./data/tokens.json # Required. How often readings are appended. More often means more rows and a finer history. METRICS_INTERVAL_MINUTES=240
You are building a lean indie version of FeedHive. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # FeedHive · indie build A scheduler for one author whose hard problems are that every network has a different and changing set of caps and formats, that a worker publishing a due post must do it exactly once even when the API times out, and that analytics history cannot be recovered after the fact so you either record it from day one or you do not have it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, a small server-rendered editor and one worker process | scheduling is a worker problem and the editor is a thin skin over a queue | | Storage | PostgreSQL for rows, media files on disk beside it, checksummed | a post is a row plus files and the files are the part that goes wrong | | Publishing | Official platform APIs only, one adapter per network | anything that drives a logged-in browser session breaks the terms and loses the account | | Scheduling | Due rows claimed by conditional update, with a per-attempt request identifier | two workers and one due post is a double publish that you cannot delete quietly | | Limits | A limits table you fill in from the documentation, with the date you read it | the caps are published, they change, and guessing them costs you a suspended app | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the worker. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Holds posts, schedules and the append-only metric history, which grows and needs real indexes. - Get it: Your package manager or a small managed instance. - Verify: Migrations apply and the worker claims a row - [ ] **A developer application per network you intend to publish to** · varies sharply by network; check each current published pricing page before you commit - Why: Each network has its own review, its own scopes and its own publishing caps, and at least one of them will refuse you. - Get it: Register an application on each network's developer portal and request the publishing permission. - Verify: A test post reaches each connected account through the API - [ ] **ffmpeg** (optional) · free - Why: Per-network renditions are the only way to stop an upload being rejected for a format you did not check. - Get it: Your package manager. - Verify: ffmpeg -version prints a version - [ ] **The limits table, filled in from current documentation** · free - Why: Every refusal in this build cites a number from this table, and a table filled in from memory produces refusals that are wrong in both directions. - Get it: Read each network's current publishing documentation and write the caps, windows and allowed types down with today's date. - Verify: Every row has a checked_on date and the editor shows it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. - analytics history and attribution - team approvals and asset workflows - protection from API and policy changes - calendar-provider edge cases and timezone correctness - reliable direct publishing across networks If one of those is essential to you, that is the reason to keep paying for FeedHive, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · FeedHive The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal posting queue that drafts per network, validates against each network's published limits, and publishes only through documented APIs to replace FeedHive. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node 22, a small server-rendered editor and one worker process · scheduling is a worker problem and the editor is a thin skin over a queue - Storage: PostgreSQL for rows, media files on disk beside it, checksummed · a post is a row plus files and the files are the part that goes wrong - Publishing: Official platform APIs only, one adapter per network · anything that drives a logged-in browser session breaks the terms and loses the account - Scheduling: Due rows claimed by conditional update, with a per-attempt request identifier · two workers and one due post is a double publish that you cannot delete quietly - Limits: A limits table you fill in from the documentation, with the date you read it · the caps are published, they change, and guessing them costs you a suspended app ### Data model (create this before Phase 1) - posts(id, title, body_master, created_at, archived_at) · the thing you wrote once - variants(id, post_id, network, body, media_ids_json, validated_at, validation_errors_json) · one row per network, because the caps and the allowed formats differ - schedules(id, variant_id, due_at_utc, author_timezone, state, claimed_by, claimed_at, attempts) · times stored in UTC with the authoring zone kept beside them - publish_attempts(id, schedule_id, attempt, request_id, http_status, remote_id, error_code, at) · every attempt including the ones that timed out - media(id, path, bytes, mime, width, height, duration_ms, checksum) · checksummed so one file is uploaded once per network and reused - network_limits(network, max_chars, max_media, allowed_mime_json, posts_per_window, window_seconds, container_ttl_seconds, checked_on) · maintained by hand, with the date you last read the documentation - quota_usage(network, window_start, count) · what you have spent against the published cap in the current moving window - metrics(remote_id, network, collected_at, metric, value) · collected on a schedule, because nobody will give you last month's numbers later ### Phase 1 · Variants that are checked before they are scheduled Build: Fill network_limits by reading each platform's current publishing documentation and record the date you read it. Validate every variant against its row: character count by the network's own counting rule, media count, and allowed media types. Show failures in the editor as specific refusals rather than a generic error, and refuse to schedule a variant that has never validated. Trap: Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. Done when: A variant exceeding its network's recorded character limit cannot be scheduled and names the limit and the date it was checked, tested. Character counting is per adapter and each adapter is tested against a fixture containing emoji, combining marks and a long URL. A media type not in the network's allowed list is refused at validation rather than at publish, tested with a PNG against an adapter that accepts only JPEG. Every network_limits row carries a checked_on date and the editor displays it. Do not build yet: No scheduling, no publishing, no media pipeline yet. ### Phase 2 · A worker that publishes exactly once Build: Claim a due schedule with a conditional update that sets state and claimed_by only if the row is still pending, and take nothing that another worker already holds. Generate a request identifier per attempt and store it before the call. On a timeout, mark the attempt unknown and reconcile by looking for the post on the network before ever trying again. Trap: Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. Done when: Two workers started against the same due row result in one publish, tested by running both against a stubbed adapter with a delay. A publish that times out leaves the attempt in an unknown state and does not retry until reconciliation has run, tested. Reconciliation finds an existing remote post and records its identifier instead of publishing again, tested with a stub that reports the post exists. A crashed worker's claim expires after a stated interval and the row becomes claimable again, tested by killing the process. Do not build yet: No quota accounting, no media upload, no metrics yet. ### Phase 3 · Living inside the published caps Build: For each network record the published publishing cap and its window. One that matters concretely: the Instagram content publishing documentation states a limit of 100 API-published posts within a 24-hour moving period, that a carousel of up to ten items counts as a single post, and that a created container expires if it is not published within 24 hours. Track usage in a moving window and hold posts back rather than letting them fail. Trap: Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. Done when: Usage is counted in a sliding window of the documented length and not per calendar day, tested by placing attempts either side of a midnight boundary. A post that would exceed the recorded cap is deferred with a reason rather than attempted, tested. A media container older than the recorded container lifetime is discarded and rebuilt rather than published into, tested with a stubbed clock. Every cap in the code reads from network_limits and no number is hard-coded in an adapter, verified by review. Do not build yet: No metrics collection, no export, no recycling yet. ### Phase 4 · Media that survives the upload Build: Checksum every file on import. Derive per-network renditions with recorded parameters and cache them by checksum and network, so the same image is prepared once. Upload through each network's documented media flow and store the remote media identifier against the rendition. Keep the original untouched. Trap: Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. Done when: The same source file scheduled to two networks produces two renditions and one stored original, tested. A retried publish reuses the cached rendition and the existing remote media identifier rather than uploading again, tested. A source whose type is not accepted by the target network is converted with recorded parameters or refused, never uploaded unchanged and hoped for, tested with a PNG against a JPEG-only adapter. Deleting a post leaves the original file until an explicit purge, tested. Do not build yet: No metrics, no export, no operations work yet. ### Phase 5 · Numbers you can only collect forwards, and a way out Build: Collect the metrics each network exposes for your own posts on a schedule and append them with the time they were collected. Never overwrite an earlier reading. Export posts, variants, schedules, attempts and metrics as newline-delimited JSON with the media manifest. When a token expires, mark that network disconnected, hold its queue, and keep every other network publishing. Trap: Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist. Done when: Metrics are appended with a collection time and no row is ever updated, asserted in the suite. An expired token marks one network disconnected, holds its queue and leaves other networks publishing, tested. Export produces every table plus a media manifest and reimports into an empty database with identical counts, tested. A post that was published is never deleted locally by a failed refresh, only marked, tested by simulating an API returning a not-found. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that drives a logged-in browser session or an undocumented endpoint, because it breaks the platform terms and the account it burns is yours. - Automatic engagement, follow loops and comment generation, which are the behaviours every network's spam policy is written about. - Team approval workflows and asset libraries, which are a permissions model and a storage product rather than a phase here. - Backfilling analytics from before you started collecting, because the networks do not expose it and inventing it would be a lie in a chart. ### README must contain - That publishing goes through documented APIs only, and what happens to an account that uses anything else - The network_limits table, where the numbers came from, and the instruction to recheck them on a stated cadence - That metrics are only ever collected forwards, so history begins the day you start and cannot be recovered - That a due post is claimed by conditional update and that a timed-out publish is reconciled rather than retried - Where media originals live, how renditions are cached, and what a purge deletes ===== AGENTS.md ===== # Agent instructions · FeedHive indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, a small server-rendered editor and one worker process, PostgreSQL for rows, media files on disk beside it, checksummed, Official platform APIs only, one adapter per network, Due rows claimed by conditional update, with a per-attempt request identifier, A limits table you fill in from the documentation, with the date you read it. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. ===== BUILD_PLAN.md ===== # Build plan · FeedHive A scheduler for one author whose hard problems are that every network has a different and changing set of caps and formats, that a worker publishing a due post must do it exactly once even when the API times out, and that analytics history cannot be recovered after the fact so you either record it from day one or you do not have it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Variants that are checked before they are scheduled A variant cannot enter the queue until it passes the limits recorded for its network. ### Steps 1. Write the limits table by hand from the documentation With the date, because these numbers move and stale ones fail at publish time. Files: `migrations/002_network_limits.sql`, `docs/limits.md` 2. Implement a counting rule per adapter String length is not what any of these networks measure. Files: `src/adapters/*/count.ts`, `tests/counting.test.ts` 3. Refuse to schedule an unvalidated variant Validation at publish time is validation that happens while you are asleep. Files: `src/schedule/create.ts`, `tests/validate-gate.test.ts` ### Done when - [ ] An over-length variant cannot be scheduled - [ ] Counting is per adapter and tested with awkward strings - [ ] A disallowed media type is refused at validation - [ ] Every limits row shows when it was last checked ### Watch out - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. ## Phase 2 · A worker that publishes exactly once A due post is published once even when two workers run and the API times out. ### Steps 1. Claim with a conditional update, not a select The row count from the update is the lock. Files: `src/worker/claim.ts`, `tests/double-claim.test.ts` 2. Record the request identifier before the call So an unknown outcome is investigable rather than guessed at. Files: `src/worker/publish.ts` 3. Reconcile unknowns before retrying Look for the post before you make a second one. Files: `src/worker/reconcile.ts`, `tests/unknown-publish.test.ts` ### Done when - [ ] Two workers publish a due post once - [ ] A timeout leaves an unknown, not a failure - [ ] Reconciliation adopts an existing remote post - [ ] An expired claim becomes claimable again ### Watch out - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. ## Phase 3 · Living inside the published caps The queue spends against each network's published cap deliberately rather than discovering it through errors. ### Steps 1. Count usage in a sliding window The documented windows are moving, not calendar days. Files: `src/limits/window.ts`, `tests/sliding-window.test.ts` 2. Defer rather than fail when the cap is reached A deferred post is recoverable; a rejected one needs a human. Files: `src/worker/claim.ts` 3. Expire containers before you publish into them A container has a published lifetime and it is shorter than a weekend. Files: `src/adapters/instagram/container.ts` ### Done when - [ ] Usage is counted in a sliding window - [ ] A capped post is deferred with a reason - [ ] An expired container is rebuilt, not used - [ ] No cap is hard-coded outside the limits table ### Watch out - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. ## Phase 4 · Media that survives the upload A file is prepared once per network, uploaded once, and never silently re-encoded into something the network rejects. ### Steps 1. Checksum on import and key everything by it The checksum is the identity of the file, not the filename. Files: `src/media/import.ts`, `migrations/003_media.sql` 2. Cache renditions by checksum and network Prepared once, uploaded once, reused on retry. Files: `src/media/render.ts`, `tests/rendition-cache.test.ts` 3. Keep the original and purge only on request Re-encoding from a rendition is how quality quietly dies. Files: `src/media/purge.ts` ### Done when - [ ] One source yields per-network renditions and one original - [ ] A retry reuses the rendition and media identifier - [ ] An unsupported type is converted or refused - [ ] Originals survive post deletion until purged ### Watch out - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. ## Not in this build - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. ## After v1, if you want it - Recycling, implemented as an evergreen post returning to the queue as a draft that you approve again rather than as an automatic repost - A limits drift checker that flags any network_limits row older than your chosen cadence - A per-network dry run that renders exactly what would be sent, which catches format mistakes before they cost a cap slot ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Posts, schedules, attempts and metrics. DATABASE_URL=postgres://feedhive@localhost/feedhive # Required. Originals and cached renditions, keyed by checksum. MEDIA_DIR=./data/media # Required. How long a claim survives a dead worker before the row is claimable again. WORKER_CLAIM_TTL_SECONDS=300 # Optional · secret. Drafting per-network variants. The app works without it and you write the variants yourself. LLM_API_KEY=sk-...redacted # Optional. Drafting stops at this figure for the month and manual editing continues. LLM_MONTHLY_CEILING=3.00 # Required · secret. Per-network OAuth tokens and refresh state. Never in the database dump you share. NETWORK_TOKENS_PATH=./data/tokens.json # Required. How often readings are appended. More often means more rows and a finer history. METRICS_INTERVAL_MINUTES=240
You are building a production product version of FeedHive. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # FeedHive · product brief ## Problem The visible AI social scheduling loop is buildable, but a credible replacement needs more than the first screen. FeedHive earns its keep through platform APIs, distribution, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A queue for one author that refuses a post before it is scheduled rather than after it fails, publishes a due post exactly once across two workers and a timeout, spends inside each network's published cap, and records metric history from the first day because nobody will supply it later. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - hosted database and worker - official platform OAuth credentials where available ## Explicit non-goals for v1 - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. - analytics history and attribution - team approvals and asset workflows - protection from API and policy changes - calendar-provider edge cases and timezone correctness - reliable direct publishing across networks ## Success criteria - An over-length or wrongly typed variant is confirmed unschedulable - Two workers are confirmed to publish a due post once - A timed-out publish is confirmed to reconcile before retrying - Quota is confirmed counted in a sliding window - Metric readings are confirmed append-only ===== BRIEF.md ===== # Build brief · FeedHive The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal posting queue that drafts per network, validates against each network's published limits, and publishes only through documented APIs to replace FeedHive. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node 22, a small server-rendered editor and one worker process · scheduling is a worker problem and the editor is a thin skin over a queue - Storage: PostgreSQL for rows, media files on disk beside it, checksummed · a post is a row plus files and the files are the part that goes wrong - Publishing: Official platform APIs only, one adapter per network · anything that drives a logged-in browser session breaks the terms and loses the account - Scheduling: Due rows claimed by conditional update, with a per-attempt request identifier · two workers and one due post is a double publish that you cannot delete quietly - Limits: A limits table you fill in from the documentation, with the date you read it · the caps are published, they change, and guessing them costs you a suspended app ### Data model (create this before Phase 1) - posts(id, title, body_master, created_at, archived_at) · the thing you wrote once - variants(id, post_id, network, body, media_ids_json, validated_at, validation_errors_json) · one row per network, because the caps and the allowed formats differ - schedules(id, variant_id, due_at_utc, author_timezone, state, claimed_by, claimed_at, attempts) · times stored in UTC with the authoring zone kept beside them - publish_attempts(id, schedule_id, attempt, request_id, http_status, remote_id, error_code, at) · every attempt including the ones that timed out - media(id, path, bytes, mime, width, height, duration_ms, checksum) · checksummed so one file is uploaded once per network and reused - network_limits(network, max_chars, max_media, allowed_mime_json, posts_per_window, window_seconds, container_ttl_seconds, checked_on) · maintained by hand, with the date you last read the documentation - quota_usage(network, window_start, count) · what you have spent against the published cap in the current moving window - metrics(remote_id, network, collected_at, metric, value) · collected on a schedule, because nobody will give you last month's numbers later ### Phase 1 · Variants that are checked before they are scheduled Build: Fill network_limits by reading each platform's current publishing documentation and record the date you read it. Validate every variant against its row: character count by the network's own counting rule, media count, and allowed media types. Show failures in the editor as specific refusals rather than a generic error, and refuse to schedule a variant that has never validated. Trap: Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. Done when: A variant exceeding its network's recorded character limit cannot be scheduled and names the limit and the date it was checked, tested. Character counting is per adapter and each adapter is tested against a fixture containing emoji, combining marks and a long URL. A media type not in the network's allowed list is refused at validation rather than at publish, tested with a PNG against an adapter that accepts only JPEG. Every network_limits row carries a checked_on date and the editor displays it. Do not build yet: No scheduling, no publishing, no media pipeline yet. ### Phase 2 · A worker that publishes exactly once Build: Claim a due schedule with a conditional update that sets state and claimed_by only if the row is still pending, and take nothing that another worker already holds. Generate a request identifier per attempt and store it before the call. On a timeout, mark the attempt unknown and reconcile by looking for the post on the network before ever trying again. Trap: Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. Done when: Two workers started against the same due row result in one publish, tested by running both against a stubbed adapter with a delay. A publish that times out leaves the attempt in an unknown state and does not retry until reconciliation has run, tested. Reconciliation finds an existing remote post and records its identifier instead of publishing again, tested with a stub that reports the post exists. A crashed worker's claim expires after a stated interval and the row becomes claimable again, tested by killing the process. Do not build yet: No quota accounting, no media upload, no metrics yet. ### Phase 3 · Living inside the published caps Build: For each network record the published publishing cap and its window. One that matters concretely: the Instagram content publishing documentation states a limit of 100 API-published posts within a 24-hour moving period, that a carousel of up to ten items counts as a single post, and that a created container expires if it is not published within 24 hours. Track usage in a moving window and hold posts back rather than letting them fail. Trap: Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. Done when: Usage is counted in a sliding window of the documented length and not per calendar day, tested by placing attempts either side of a midnight boundary. A post that would exceed the recorded cap is deferred with a reason rather than attempted, tested. A media container older than the recorded container lifetime is discarded and rebuilt rather than published into, tested with a stubbed clock. Every cap in the code reads from network_limits and no number is hard-coded in an adapter, verified by review. Do not build yet: No metrics collection, no export, no recycling yet. ### Phase 4 · Media that survives the upload Build: Checksum every file on import. Derive per-network renditions with recorded parameters and cache them by checksum and network, so the same image is prepared once. Upload through each network's documented media flow and store the remote media identifier against the rendition. Keep the original untouched. Trap: Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. Done when: The same source file scheduled to two networks produces two renditions and one stored original, tested. A retried publish reuses the cached rendition and the existing remote media identifier rather than uploading again, tested. A source whose type is not accepted by the target network is converted with recorded parameters or refused, never uploaded unchanged and hoped for, tested with a PNG against a JPEG-only adapter. Deleting a post leaves the original file until an explicit purge, tested. Do not build yet: No metrics, no export, no operations work yet. ### Phase 5 · Numbers you can only collect forwards, and a way out Build: Collect the metrics each network exposes for your own posts on a schedule and append them with the time they were collected. Never overwrite an earlier reading. Export posts, variants, schedules, attempts and metrics as newline-delimited JSON with the media manifest. When a token expires, mark that network disconnected, hold its queue, and keep every other network publishing. Trap: Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist. Done when: Metrics are appended with a collection time and no row is ever updated, asserted in the suite. An expired token marks one network disconnected, holds its queue and leaves other networks publishing, tested. Export produces every table plus a media manifest and reimports into an empty database with identical counts, tested. A post that was published is never deleted locally by a failed refresh, only marked, tested by simulating an API returning a not-found. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that drives a logged-in browser session or an undocumented endpoint, because it breaks the platform terms and the account it burns is yours. - Automatic engagement, follow loops and comment generation, which are the behaviours every network's spam policy is written about. - Team approval workflows and asset libraries, which are a permissions model and a storage product rather than a phase here. - Backfilling analytics from before you started collecting, because the networks do not expose it and inventing it would be a lie in a chart. ### README must contain - That publishing goes through documented APIs only, and what happens to an account that uses anything else - The network_limits table, where the numbers came from, and the instruction to recheck them on a stated cadence - That metrics are only ever collected forwards, so history begins the day you start and cannot be recovered - That a due post is claimed by conditional update and that a timed-out publish is reconciled rather than retried - Where media originals live, how renditions are cached, and what a purge deletes ===== ARCHITECTURE.md ===== # Architecture · FeedHive ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, a small server-rendered editor and one worker process | scheduling is a worker problem and the editor is a thin skin over a queue | | Storage | PostgreSQL for rows, media files on disk beside it, checksummed | a post is a row plus files and the files are the part that goes wrong | | Publishing | Official platform APIs only, one adapter per network | anything that drives a logged-in browser session breaks the terms and loses the account | | Scheduling | Due rows claimed by conditional update, with a per-attempt request identifier | two workers and one due post is a double publish that you cannot delete quietly | | Limits | A limits table you fill in from the documentation, with the date you read it | the caps are published, they change, and guessing them costs you a suspended app | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Editor | Posts, variants and validation feedback. | Any front end, provided it cannot schedule an unvalidated variant. | | Adapters | Per-network counting, formats, publishing and reconciliation. | One per network, added independently, each with its own tests. | | Worker | Claiming, attempts, unknown-state reconciliation. | Any runner, provided claiming is a conditional update. | | Media | Checksums, renditions and upload identifiers. | Any encoder, provided renditions are cached by checksum and network. | | Metrics | Append-only readings with collection times. | Not swappable for a current-value column, which loses the history. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Posts, schedules, attempts and metrics. - `MEDIA_DIR` · required · Originals and cached renditions, keyed by checksum. - `WORKER_CLAIM_TTL_SECONDS` · required · How long a claim survives a dead worker before the row is claimable again. - `LLM_API_KEY` · optional, secret · Drafting per-network variants. The app works without it and you write the variants yourself. - `LLM_MONTHLY_CEILING` · optional · Drafting stops at this figure for the month and manual editing continues. - `NETWORK_TOKENS_PATH` · required, secret · Per-network OAuth tokens and refresh state. Never in the database dump you share. - `METRICS_INTERVAL_MINUTES` · required · How often readings are appended. More often means more rows and a finer history. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · FeedHive product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, a small server-rendered editor and one worker process, PostgreSQL for rows, media files on disk beside it, checksummed, Official platform APIs only, one adapter per network, Due rows claimed by conditional update, with a per-attempt request identifier, A limits table you fill in from the documentation, with the date you read it. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. - Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist. ===== MILESTONES.md ===== # Delivery milestones · FeedHive Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Variants that are checked before they are scheduled A variant cannot enter the queue until it passes the limits recorded for its network. ### Steps 1. Write the limits table by hand from the documentation With the date, because these numbers move and stale ones fail at publish time. Files: `migrations/002_network_limits.sql`, `docs/limits.md` 2. Implement a counting rule per adapter String length is not what any of these networks measure. Files: `src/adapters/*/count.ts`, `tests/counting.test.ts` 3. Refuse to schedule an unvalidated variant Validation at publish time is validation that happens while you are asleep. Files: `src/schedule/create.ts`, `tests/validate-gate.test.ts` ### Done when - [ ] An over-length variant cannot be scheduled - [ ] Counting is per adapter and tested with awkward strings - [ ] A disallowed media type is refused at validation - [ ] Every limits row shows when it was last checked ### Watch out - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. ## M2 · A worker that publishes exactly once A due post is published once even when two workers run and the API times out. ### Steps 1. Claim with a conditional update, not a select The row count from the update is the lock. Files: `src/worker/claim.ts`, `tests/double-claim.test.ts` 2. Record the request identifier before the call So an unknown outcome is investigable rather than guessed at. Files: `src/worker/publish.ts` 3. Reconcile unknowns before retrying Look for the post before you make a second one. Files: `src/worker/reconcile.ts`, `tests/unknown-publish.test.ts` ### Done when - [ ] Two workers publish a due post once - [ ] A timeout leaves an unknown, not a failure - [ ] Reconciliation adopts an existing remote post - [ ] An expired claim becomes claimable again ### Watch out - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. ## M3 · Living inside the published caps The queue spends against each network's published cap deliberately rather than discovering it through errors. ### Steps 1. Count usage in a sliding window The documented windows are moving, not calendar days. Files: `src/limits/window.ts`, `tests/sliding-window.test.ts` 2. Defer rather than fail when the cap is reached A deferred post is recoverable; a rejected one needs a human. Files: `src/worker/claim.ts` 3. Expire containers before you publish into them A container has a published lifetime and it is shorter than a weekend. Files: `src/adapters/instagram/container.ts` ### Done when - [ ] Usage is counted in a sliding window - [ ] A capped post is deferred with a reason - [ ] An expired container is rebuilt, not used - [ ] No cap is hard-coded outside the limits table ### Watch out - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. ## M4 · Media that survives the upload A file is prepared once per network, uploaded once, and never silently re-encoded into something the network rejects. ### Steps 1. Checksum on import and key everything by it The checksum is the identity of the file, not the filename. Files: `src/media/import.ts`, `migrations/003_media.sql` 2. Cache renditions by checksum and network Prepared once, uploaded once, reused on retry. Files: `src/media/render.ts`, `tests/rendition-cache.test.ts` 3. Keep the original and purge only on request Re-encoding from a rendition is how quality quietly dies. Files: `src/media/purge.ts` ### Done when - [ ] One source yields per-network renditions and one original - [ ] A retry reuses the rendition and media identifier - [ ] An unsupported type is converted or refused - [ ] Originals survive post deletion until purged ### Watch out - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. ## M5 · Numbers you can only collect forwards, and a way out (production only) Metrics are recorded from the first publish, everything exports, and a broken token degrades the app instead of stopping it. ### Steps 1. Append metric readings, never update them Nobody will give you last month's numbers later. Files: `src/metrics/collect.ts`, `tests/metrics-append.test.ts` 2. Degrade per network on token expiry One dead token should not stop the other four queues. Files: `src/adapters/registry.ts`, `tests/degrade.test.ts` 3. Export everything including the media manifest An export without the files is half an export. Files: `src/export/dump.ts`, `scripts/reimport.sh` ### Done when - [ ] Metric readings are append-only - [ ] One expired token holds only its own queue - [ ] Export and reimport preserve every count - [ ] A not-found from an API never deletes local history ### Watch out - Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist. ===== OPERATIONS.md ===== # Operations · FeedHive ## Backup The database nightly and the media originals weekly. Renditions are derivable and need not be backed up, which is worth stating so the backup stays small enough to actually run. ## Restore Restore the database, reimport the media manifest, and confirm that every published schedule still carries its remote identifier. A restore that loses remote identifiers will republish things that are already live, so check that before starting the worker. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Deferred posts per network, the age of the oldest unknown attempt, quota usage against each recorded cap, and the days since each network_limits row was checked. A limits row that has not been rechecked in months is the next outage. ## Incident checklist The incident here is a duplicate live post. Diagnosis starts from publish_attempts, where the unknown outcome and the reconciliation result are recorded, and the fix is almost always that somebody retried an unknown instead of reconciling it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An over-length or wrongly typed variant is confirmed unschedulable - [ ] Two workers are confirmed to publish a due post once - [ ] A timed-out publish is confirmed to reconcile before retrying - [ ] Quota is confirmed counted in a sliding window - [ ] Metric readings are confirmed append-only ## Launch constraint Do not market omitted FeedHive capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Posts, schedules, attempts and metrics. DATABASE_URL=postgres://feedhive@localhost/feedhive # Required. Originals and cached renditions, keyed by checksum. MEDIA_DIR=./data/media # Required. How long a claim survives a dead worker before the row is claimable again. WORKER_CLAIM_TTL_SECONDS=300 # Optional · secret. Drafting per-network variants. The app works without it and you write the variants yourself. LLM_API_KEY=sk-...redacted # Optional. Drafting stops at this figure for the month and manual editing continues. LLM_MONTHLY_CEILING=3.00 # Required · secret. Per-network OAuth tokens and refresh state. Never in the database dump you share. NETWORK_TOKENS_PATH=./data/tokens.json # Required. How often readings are appended. More often means more rows and a finer history. METRICS_INTERVAL_MINUTES=240
# FeedHive · indie build A scheduler for one author whose hard problems are that every network has a different and changing set of caps and formats, that a worker publishing a due post must do it exactly once even when the API times out, and that analytics history cannot be recovered after the fact so you either record it from day one or you do not have it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, a small server-rendered editor and one worker process | scheduling is a worker problem and the editor is a thin skin over a queue | | Storage | PostgreSQL for rows, media files on disk beside it, checksummed | a post is a row plus files and the files are the part that goes wrong | | Publishing | Official platform APIs only, one adapter per network | anything that drives a logged-in browser session breaks the terms and loses the account | | Scheduling | Due rows claimed by conditional update, with a per-attempt request identifier | two workers and one due post is a double publish that you cannot delete quietly | | Limits | A limits table you fill in from the documentation, with the date you read it | the caps are published, they change, and guessing them costs you a suspended app | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the worker. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Holds posts, schedules and the append-only metric history, which grows and needs real indexes. - Get it: Your package manager or a small managed instance. - Verify: Migrations apply and the worker claims a row - [ ] **A developer application per network you intend to publish to** · varies sharply by network; check each current published pricing page before you commit - Why: Each network has its own review, its own scopes and its own publishing caps, and at least one of them will refuse you. - Get it: Register an application on each network's developer portal and request the publishing permission. - Verify: A test post reaches each connected account through the API - [ ] **ffmpeg** (optional) · free - Why: Per-network renditions are the only way to stop an upload being rejected for a format you did not check. - Get it: Your package manager. - Verify: ffmpeg -version prints a version - [ ] **The limits table, filled in from current documentation** · free - Why: Every refusal in this build cites a number from this table, and a table filled in from memory produces refusals that are wrong in both directions. - Get it: Read each network's current publishing documentation and write the caps, windows and allowed types down with today's date. - Verify: Every row has a checked_on date and the editor shows it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. - analytics history and attribution - team approvals and asset workflows - protection from API and policy changes - calendar-provider edge cases and timezone correctness - reliable direct publishing across networks If one of those is essential to you, that is the reason to keep paying for FeedHive, and the README should say so rather than pretend.
# Build brief · FeedHive The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal posting queue that drafts per network, validates against each network's published limits, and publishes only through documented APIs to replace FeedHive. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node 22, a small server-rendered editor and one worker process · scheduling is a worker problem and the editor is a thin skin over a queue - Storage: PostgreSQL for rows, media files on disk beside it, checksummed · a post is a row plus files and the files are the part that goes wrong - Publishing: Official platform APIs only, one adapter per network · anything that drives a logged-in browser session breaks the terms and loses the account - Scheduling: Due rows claimed by conditional update, with a per-attempt request identifier · two workers and one due post is a double publish that you cannot delete quietly - Limits: A limits table you fill in from the documentation, with the date you read it · the caps are published, they change, and guessing them costs you a suspended app ### Data model (create this before Phase 1) - posts(id, title, body_master, created_at, archived_at) · the thing you wrote once - variants(id, post_id, network, body, media_ids_json, validated_at, validation_errors_json) · one row per network, because the caps and the allowed formats differ - schedules(id, variant_id, due_at_utc, author_timezone, state, claimed_by, claimed_at, attempts) · times stored in UTC with the authoring zone kept beside them - publish_attempts(id, schedule_id, attempt, request_id, http_status, remote_id, error_code, at) · every attempt including the ones that timed out - media(id, path, bytes, mime, width, height, duration_ms, checksum) · checksummed so one file is uploaded once per network and reused - network_limits(network, max_chars, max_media, allowed_mime_json, posts_per_window, window_seconds, container_ttl_seconds, checked_on) · maintained by hand, with the date you last read the documentation - quota_usage(network, window_start, count) · what you have spent against the published cap in the current moving window - metrics(remote_id, network, collected_at, metric, value) · collected on a schedule, because nobody will give you last month's numbers later ### Phase 1 · Variants that are checked before they are scheduled Build: Fill network_limits by reading each platform's current publishing documentation and record the date you read it. Validate every variant against its row: character count by the network's own counting rule, media count, and allowed media types. Show failures in the editor as specific refusals rather than a generic error, and refuse to schedule a variant that has never validated. Trap: Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. Done when: A variant exceeding its network's recorded character limit cannot be scheduled and names the limit and the date it was checked, tested. Character counting is per adapter and each adapter is tested against a fixture containing emoji, combining marks and a long URL. A media type not in the network's allowed list is refused at validation rather than at publish, tested with a PNG against an adapter that accepts only JPEG. Every network_limits row carries a checked_on date and the editor displays it. Do not build yet: No scheduling, no publishing, no media pipeline yet. ### Phase 2 · A worker that publishes exactly once Build: Claim a due schedule with a conditional update that sets state and claimed_by only if the row is still pending, and take nothing that another worker already holds. Generate a request identifier per attempt and store it before the call. On a timeout, mark the attempt unknown and reconcile by looking for the post on the network before ever trying again. Trap: Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. Done when: Two workers started against the same due row result in one publish, tested by running both against a stubbed adapter with a delay. A publish that times out leaves the attempt in an unknown state and does not retry until reconciliation has run, tested. Reconciliation finds an existing remote post and records its identifier instead of publishing again, tested with a stub that reports the post exists. A crashed worker's claim expires after a stated interval and the row becomes claimable again, tested by killing the process. Do not build yet: No quota accounting, no media upload, no metrics yet. ### Phase 3 · Living inside the published caps Build: For each network record the published publishing cap and its window. One that matters concretely: the Instagram content publishing documentation states a limit of 100 API-published posts within a 24-hour moving period, that a carousel of up to ten items counts as a single post, and that a created container expires if it is not published within 24 hours. Track usage in a moving window and hold posts back rather than letting them fail. Trap: Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. Done when: Usage is counted in a sliding window of the documented length and not per calendar day, tested by placing attempts either side of a midnight boundary. A post that would exceed the recorded cap is deferred with a reason rather than attempted, tested. A media container older than the recorded container lifetime is discarded and rebuilt rather than published into, tested with a stubbed clock. Every cap in the code reads from network_limits and no number is hard-coded in an adapter, verified by review. Do not build yet: No metrics collection, no export, no recycling yet. ### Phase 4 · Media that survives the upload Build: Checksum every file on import. Derive per-network renditions with recorded parameters and cache them by checksum and network, so the same image is prepared once. Upload through each network's documented media flow and store the remote media identifier against the rendition. Keep the original untouched. Trap: Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. Done when: The same source file scheduled to two networks produces two renditions and one stored original, tested. A retried publish reuses the cached rendition and the existing remote media identifier rather than uploading again, tested. A source whose type is not accepted by the target network is converted with recorded parameters or refused, never uploaded unchanged and hoped for, tested with a PNG against a JPEG-only adapter. Deleting a post leaves the original file until an explicit purge, tested. Do not build yet: No metrics, no export, no operations work yet. ### Phase 5 · Numbers you can only collect forwards, and a way out Build: Collect the metrics each network exposes for your own posts on a schedule and append them with the time they were collected. Never overwrite an earlier reading. Export posts, variants, schedules, attempts and metrics as newline-delimited JSON with the media manifest. When a token expires, mark that network disconnected, hold its queue, and keep every other network publishing. Trap: Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist. Done when: Metrics are appended with a collection time and no row is ever updated, asserted in the suite. An expired token marks one network disconnected, holds its queue and leaves other networks publishing, tested. Export produces every table plus a media manifest and reimports into an empty database with identical counts, tested. A post that was published is never deleted locally by a failed refresh, only marked, tested by simulating an API returning a not-found. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that drives a logged-in browser session or an undocumented endpoint, because it breaks the platform terms and the account it burns is yours. - Automatic engagement, follow loops and comment generation, which are the behaviours every network's spam policy is written about. - Team approval workflows and asset libraries, which are a permissions model and a storage product rather than a phase here. - Backfilling analytics from before you started collecting, because the networks do not expose it and inventing it would be a lie in a chart. ### README must contain - That publishing goes through documented APIs only, and what happens to an account that uses anything else - The network_limits table, where the numbers came from, and the instruction to recheck them on a stated cadence - That metrics are only ever collected forwards, so history begins the day you start and cannot be recovered - That a due post is claimed by conditional update and that a timed-out publish is reconciled rather than retried - Where media originals live, how renditions are cached, and what a purge deletes
# Agent instructions · FeedHive indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, a small server-rendered editor and one worker process, PostgreSQL for rows, media files on disk beside it, checksummed, Official platform APIs only, one adapter per network, Due rows claimed by conditional update, with a per-attempt request identifier, A limits table you fill in from the documentation, with the date you read it. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair.
# Build plan · FeedHive A scheduler for one author whose hard problems are that every network has a different and changing set of caps and formats, that a worker publishing a due post must do it exactly once even when the API times out, and that analytics history cannot be recovered after the fact so you either record it from day one or you do not have it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Variants that are checked before they are scheduled A variant cannot enter the queue until it passes the limits recorded for its network. ### Steps 1. Write the limits table by hand from the documentation With the date, because these numbers move and stale ones fail at publish time. Files: `migrations/002_network_limits.sql`, `docs/limits.md` 2. Implement a counting rule per adapter String length is not what any of these networks measure. Files: `src/adapters/*/count.ts`, `tests/counting.test.ts` 3. Refuse to schedule an unvalidated variant Validation at publish time is validation that happens while you are asleep. Files: `src/schedule/create.ts`, `tests/validate-gate.test.ts` ### Done when - [ ] An over-length variant cannot be scheduled - [ ] Counting is per adapter and tested with awkward strings - [ ] A disallowed media type is refused at validation - [ ] Every limits row shows when it was last checked ### Watch out - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. ## Phase 2 · A worker that publishes exactly once A due post is published once even when two workers run and the API times out. ### Steps 1. Claim with a conditional update, not a select The row count from the update is the lock. Files: `src/worker/claim.ts`, `tests/double-claim.test.ts` 2. Record the request identifier before the call So an unknown outcome is investigable rather than guessed at. Files: `src/worker/publish.ts` 3. Reconcile unknowns before retrying Look for the post before you make a second one. Files: `src/worker/reconcile.ts`, `tests/unknown-publish.test.ts` ### Done when - [ ] Two workers publish a due post once - [ ] A timeout leaves an unknown, not a failure - [ ] Reconciliation adopts an existing remote post - [ ] An expired claim becomes claimable again ### Watch out - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. ## Phase 3 · Living inside the published caps The queue spends against each network's published cap deliberately rather than discovering it through errors. ### Steps 1. Count usage in a sliding window The documented windows are moving, not calendar days. Files: `src/limits/window.ts`, `tests/sliding-window.test.ts` 2. Defer rather than fail when the cap is reached A deferred post is recoverable; a rejected one needs a human. Files: `src/worker/claim.ts` 3. Expire containers before you publish into them A container has a published lifetime and it is shorter than a weekend. Files: `src/adapters/instagram/container.ts` ### Done when - [ ] Usage is counted in a sliding window - [ ] A capped post is deferred with a reason - [ ] An expired container is rebuilt, not used - [ ] No cap is hard-coded outside the limits table ### Watch out - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. ## Phase 4 · Media that survives the upload A file is prepared once per network, uploaded once, and never silently re-encoded into something the network rejects. ### Steps 1. Checksum on import and key everything by it The checksum is the identity of the file, not the filename. Files: `src/media/import.ts`, `migrations/003_media.sql` 2. Cache renditions by checksum and network Prepared once, uploaded once, reused on retry. Files: `src/media/render.ts`, `tests/rendition-cache.test.ts` 3. Keep the original and purge only on request Re-encoding from a rendition is how quality quietly dies. Files: `src/media/purge.ts` ### Done when - [ ] One source yields per-network renditions and one original - [ ] A retry reuses the rendition and media identifier - [ ] An unsupported type is converted or refused - [ ] Originals survive post deletion until purged ### Watch out - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. ## Not in this build - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. ## After v1, if you want it - Recycling, implemented as an evergreen post returning to the queue as a draft that you approve again rather than as an automatic repost - A limits drift checker that flags any network_limits row older than your chosen cadence - A per-network dry run that renders exactly what would be sent, which catches format mistakes before they cost a cap slot
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Posts, schedules, attempts and metrics. DATABASE_URL=postgres://feedhive@localhost/feedhive # Required. Originals and cached renditions, keyed by checksum. MEDIA_DIR=./data/media # Required. How long a claim survives a dead worker before the row is claimable again. WORKER_CLAIM_TTL_SECONDS=300 # Optional · secret. Drafting per-network variants. The app works without it and you write the variants yourself. LLM_API_KEY=sk-...redacted # Optional. Drafting stops at this figure for the month and manual editing continues. LLM_MONTHLY_CEILING=3.00 # Required · secret. Per-network OAuth tokens and refresh state. Never in the database dump you share. NETWORK_TOKENS_PATH=./data/tokens.json # Required. How often readings are appended. More often means more rows and a finer history. METRICS_INTERVAL_MINUTES=240
# FeedHive · product brief ## Problem The visible AI social scheduling loop is buildable, but a credible replacement needs more than the first screen. FeedHive earns its keep through platform APIs, distribution, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A queue for one author that refuses a post before it is scheduled rather than after it fails, publishes a due post exactly once across two workers and a timeout, spends inside each network's published cap, and records metric history from the first day because nobody will supply it later. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - hosted database and worker - official platform OAuth credentials where available ## Explicit non-goals for v1 - Anything driving a logged-in browser session. - Automatic engagement and comment generation. - Team approvals and shared asset libraries. - Backfilled analytics from before collection started. - analytics history and attribution - team approvals and asset workflows - protection from API and policy changes - calendar-provider edge cases and timezone correctness - reliable direct publishing across networks ## Success criteria - An over-length or wrongly typed variant is confirmed unschedulable - Two workers are confirmed to publish a due post once - A timed-out publish is confirmed to reconcile before retrying - Quota is confirmed counted in a sliding window - Metric readings are confirmed append-only
# Build brief · FeedHive The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a personal posting queue that drafts per network, validates against each network's published limits, and publishes only through documented APIs to replace FeedHive. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Node 22, a small server-rendered editor and one worker process · scheduling is a worker problem and the editor is a thin skin over a queue - Storage: PostgreSQL for rows, media files on disk beside it, checksummed · a post is a row plus files and the files are the part that goes wrong - Publishing: Official platform APIs only, one adapter per network · anything that drives a logged-in browser session breaks the terms and loses the account - Scheduling: Due rows claimed by conditional update, with a per-attempt request identifier · two workers and one due post is a double publish that you cannot delete quietly - Limits: A limits table you fill in from the documentation, with the date you read it · the caps are published, they change, and guessing them costs you a suspended app ### Data model (create this before Phase 1) - posts(id, title, body_master, created_at, archived_at) · the thing you wrote once - variants(id, post_id, network, body, media_ids_json, validated_at, validation_errors_json) · one row per network, because the caps and the allowed formats differ - schedules(id, variant_id, due_at_utc, author_timezone, state, claimed_by, claimed_at, attempts) · times stored in UTC with the authoring zone kept beside them - publish_attempts(id, schedule_id, attempt, request_id, http_status, remote_id, error_code, at) · every attempt including the ones that timed out - media(id, path, bytes, mime, width, height, duration_ms, checksum) · checksummed so one file is uploaded once per network and reused - network_limits(network, max_chars, max_media, allowed_mime_json, posts_per_window, window_seconds, container_ttl_seconds, checked_on) · maintained by hand, with the date you last read the documentation - quota_usage(network, window_start, count) · what you have spent against the published cap in the current moving window - metrics(remote_id, network, collected_at, metric, value) · collected on a schedule, because nobody will give you last month's numbers later ### Phase 1 · Variants that are checked before they are scheduled Build: Fill network_limits by reading each platform's current publishing documentation and record the date you read it. Validate every variant against its row: character count by the network's own counting rule, media count, and allowed media types. Show failures in the editor as specific refusals rather than a generic error, and refuse to schedule a variant that has never validated. Trap: Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. Done when: A variant exceeding its network's recorded character limit cannot be scheduled and names the limit and the date it was checked, tested. Character counting is per adapter and each adapter is tested against a fixture containing emoji, combining marks and a long URL. A media type not in the network's allowed list is refused at validation rather than at publish, tested with a PNG against an adapter that accepts only JPEG. Every network_limits row carries a checked_on date and the editor displays it. Do not build yet: No scheduling, no publishing, no media pipeline yet. ### Phase 2 · A worker that publishes exactly once Build: Claim a due schedule with a conditional update that sets state and claimed_by only if the row is still pending, and take nothing that another worker already holds. Generate a request identifier per attempt and store it before the call. On a timeout, mark the attempt unknown and reconcile by looking for the post on the network before ever trying again. Trap: Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. Done when: Two workers started against the same due row result in one publish, tested by running both against a stubbed adapter with a delay. A publish that times out leaves the attempt in an unknown state and does not retry until reconciliation has run, tested. Reconciliation finds an existing remote post and records its identifier instead of publishing again, tested with a stub that reports the post exists. A crashed worker's claim expires after a stated interval and the row becomes claimable again, tested by killing the process. Do not build yet: No quota accounting, no media upload, no metrics yet. ### Phase 3 · Living inside the published caps Build: For each network record the published publishing cap and its window. One that matters concretely: the Instagram content publishing documentation states a limit of 100 API-published posts within a 24-hour moving period, that a carousel of up to ten items counts as a single post, and that a created container expires if it is not published within 24 hours. Track usage in a moving window and hold posts back rather than letting them fail. Trap: Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. Done when: Usage is counted in a sliding window of the documented length and not per calendar day, tested by placing attempts either side of a midnight boundary. A post that would exceed the recorded cap is deferred with a reason rather than attempted, tested. A media container older than the recorded container lifetime is discarded and rebuilt rather than published into, tested with a stubbed clock. Every cap in the code reads from network_limits and no number is hard-coded in an adapter, verified by review. Do not build yet: No metrics collection, no export, no recycling yet. ### Phase 4 · Media that survives the upload Build: Checksum every file on import. Derive per-network renditions with recorded parameters and cache them by checksum and network, so the same image is prepared once. Upload through each network's documented media flow and store the remote media identifier against the rendition. Keep the original untouched. Trap: Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. Done when: The same source file scheduled to two networks produces two renditions and one stored original, tested. A retried publish reuses the cached rendition and the existing remote media identifier rather than uploading again, tested. A source whose type is not accepted by the target network is converted with recorded parameters or refused, never uploaded unchanged and hoped for, tested with a PNG against a JPEG-only adapter. Deleting a post leaves the original file until an explicit purge, tested. Do not build yet: No metrics, no export, no operations work yet. ### Phase 5 · Numbers you can only collect forwards, and a way out Build: Collect the metrics each network exposes for your own posts on a schedule and append them with the time they were collected. Never overwrite an earlier reading. Export posts, variants, schedules, attempts and metrics as newline-delimited JSON with the media manifest. When a token expires, mark that network disconnected, hold its queue, and keep every other network publishing. Trap: Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist. Done when: Metrics are appended with a collection time and no row is ever updated, asserted in the suite. An expired token marks one network disconnected, holds its queue and leaves other networks publishing, tested. Export produces every table plus a media manifest and reimports into an empty database with identical counts, tested. A post that was published is never deleted locally by a failed refresh, only marked, tested by simulating an API returning a not-found. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything that drives a logged-in browser session or an undocumented endpoint, because it breaks the platform terms and the account it burns is yours. - Automatic engagement, follow loops and comment generation, which are the behaviours every network's spam policy is written about. - Team approval workflows and asset libraries, which are a permissions model and a storage product rather than a phase here. - Backfilling analytics from before you started collecting, because the networks do not expose it and inventing it would be a lie in a chart. ### README must contain - That publishing goes through documented APIs only, and what happens to an account that uses anything else - The network_limits table, where the numbers came from, and the instruction to recheck them on a stated cadence - That metrics are only ever collected forwards, so history begins the day you start and cannot be recovered - That a due post is claimed by conditional update and that a timed-out publish is reconciled rather than retried - Where media originals live, how renditions are cached, and what a purge deletes
# Architecture · FeedHive ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, a small server-rendered editor and one worker process | scheduling is a worker problem and the editor is a thin skin over a queue | | Storage | PostgreSQL for rows, media files on disk beside it, checksummed | a post is a row plus files and the files are the part that goes wrong | | Publishing | Official platform APIs only, one adapter per network | anything that drives a logged-in browser session breaks the terms and loses the account | | Scheduling | Due rows claimed by conditional update, with a per-attempt request identifier | two workers and one due post is a double publish that you cannot delete quietly | | Limits | A limits table you fill in from the documentation, with the date you read it | the caps are published, they change, and guessing them costs you a suspended app | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Editor | Posts, variants and validation feedback. | Any front end, provided it cannot schedule an unvalidated variant. | | Adapters | Per-network counting, formats, publishing and reconciliation. | One per network, added independently, each with its own tests. | | Worker | Claiming, attempts, unknown-state reconciliation. | Any runner, provided claiming is a conditional update. | | Media | Checksums, renditions and upload identifiers. | Any encoder, provided renditions are cached by checksum and network. | | Metrics | Append-only readings with collection times. | Not swappable for a current-value column, which loses the history. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Posts, schedules, attempts and metrics. - `MEDIA_DIR` · required · Originals and cached renditions, keyed by checksum. - `WORKER_CLAIM_TTL_SECONDS` · required · How long a claim survives a dead worker before the row is claimable again. - `LLM_API_KEY` · optional, secret · Drafting per-network variants. The app works without it and you write the variants yourself. - `LLM_MONTHLY_CEILING` · optional · Drafting stops at this figure for the month and manual editing continues. - `NETWORK_TOKENS_PATH` · required, secret · Per-network OAuth tokens and refresh state. Never in the database dump you share. - `METRICS_INTERVAL_MINUTES` · required · How often readings are appended. More often means more rows and a finer history. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · FeedHive product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, a small server-rendered editor and one worker process, PostgreSQL for rows, media files on disk beside it, checksummed, Official platform APIs only, one adapter per network, Due rows claimed by conditional update, with a per-attempt request identifier, A limits table you fill in from the documentation, with the date you read it. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. - Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist.
# Delivery milestones · FeedHive Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Variants that are checked before they are scheduled A variant cannot enter the queue until it passes the limits recorded for its network. ### Steps 1. Write the limits table by hand from the documentation With the date, because these numbers move and stale ones fail at publish time. Files: `migrations/002_network_limits.sql`, `docs/limits.md` 2. Implement a counting rule per adapter String length is not what any of these networks measure. Files: `src/adapters/*/count.ts`, `tests/counting.test.ts` 3. Refuse to schedule an unvalidated variant Validation at publish time is validation that happens while you are asleep. Files: `src/schedule/create.ts`, `tests/validate-gate.test.ts` ### Done when - [ ] An over-length variant cannot be scheduled - [ ] Counting is per adapter and tested with awkward strings - [ ] A disallowed media type is refused at validation - [ ] Every limits row shows when it was last checked ### Watch out - Counting characters with the string length of your runtime will pass a post that the network rejects, because some networks count code points, some count after shortening links, and emoji and accented characters are the ones that expose the difference. Implement the counting rule per adapter and unit test each one against a string of emoji, combining characters and a long link. ## M2 · A worker that publishes exactly once A due post is published once even when two workers run and the API times out. ### Steps 1. Claim with a conditional update, not a select The row count from the update is the lock. Files: `src/worker/claim.ts`, `tests/double-claim.test.ts` 2. Record the request identifier before the call So an unknown outcome is investigable rather than guessed at. Files: `src/worker/publish.ts` 3. Reconcile unknowns before retrying Look for the post before you make a second one. Files: `src/worker/reconcile.ts`, `tests/unknown-publish.test.ts` ### Done when - [ ] Two workers publish a due post once - [ ] A timeout leaves an unknown, not a failure - [ ] Reconciliation adopts an existing remote post - [ ] An expired claim becomes claimable again ### Watch out - Selecting due rows and then updating them is a race that publishes twice the first time two workers overlap, and the second copy has to be deleted by hand in front of an audience. Claim with a conditional update whose row count tells you whether you won, and treat a timed-out publish as unknown rather than failed, because retrying an unknown is exactly how the duplicate appears. ## M3 · Living inside the published caps The queue spends against each network's published cap deliberately rather than discovering it through errors. ### Steps 1. Count usage in a sliding window The documented windows are moving, not calendar days. Files: `src/limits/window.ts`, `tests/sliding-window.test.ts` 2. Defer rather than fail when the cap is reached A deferred post is recoverable; a rejected one needs a human. Files: `src/worker/claim.ts` 3. Expire containers before you publish into them A container has a published lifetime and it is shorter than a weekend. Files: `src/adapters/instagram/container.ts` ### Done when - [ ] Usage is counted in a sliding window - [ ] A capped post is deferred with a reason - [ ] An expired container is rebuilt, not used - [ ] No cap is hard-coded outside the limits table ### Watch out - Treating the cap as a daily reset at midnight will let a burst through at 00:01 and then fail the rest of the day, because the documented window is a moving one rather than a calendar day. Count attempts in a sliding window of the documented length, and separately expire any created container before the documented lifetime rather than publishing into a container that has already gone. ## M4 · Media that survives the upload A file is prepared once per network, uploaded once, and never silently re-encoded into something the network rejects. ### Steps 1. Checksum on import and key everything by it The checksum is the identity of the file, not the filename. Files: `src/media/import.ts`, `migrations/003_media.sql` 2. Cache renditions by checksum and network Prepared once, uploaded once, reused on retry. Files: `src/media/render.ts`, `tests/rendition-cache.test.ts` 3. Keep the original and purge only on request Re-encoding from a rendition is how quality quietly dies. Files: `src/media/purge.ts` ### Done when - [ ] One source yields per-network renditions and one original - [ ] A retry reuses the rendition and media identifier - [ ] An unsupported type is converted or refused - [ ] Originals survive post deletion until purged ### Watch out - Re-encoding on every publish attempt means a retry produces a byte-different file, a second upload, and two media identifiers for what the author thinks is one picture, which shows up as a duplicate in a carousel. Cache the rendition by source checksum and network, and make the upload step idempotent on that pair. ## M5 · Numbers you can only collect forwards, and a way out (production only) Metrics are recorded from the first publish, everything exports, and a broken token degrades the app instead of stopping it. ### Steps 1. Append metric readings, never update them Nobody will give you last month's numbers later. Files: `src/metrics/collect.ts`, `tests/metrics-append.test.ts` 2. Degrade per network on token expiry One dead token should not stop the other four queues. Files: `src/adapters/registry.ts`, `tests/degrade.test.ts` 3. Export everything including the media manifest An export without the files is half an export. Files: `src/export/dump.ts`, `scripts/reimport.sh` ### Done when - [ ] Metric readings are append-only - [ ] One expired token holds only its own queue - [ ] Export and reimport preserve every count - [ ] A not-found from an API never deletes local history ### Watch out - Reading a post's metrics once and storing a single current value gives you no way to answer what happened in the first week, and no network will hand you that history later, so the question is unanswerable forever. Append every reading with its collection time, and accept that the table grows, because it is the only historical record that will ever exist.
# Operations · FeedHive ## Backup The database nightly and the media originals weekly. Renditions are derivable and need not be backed up, which is worth stating so the backup stays small enough to actually run. ## Restore Restore the database, reimport the media manifest, and confirm that every published schedule still carries its remote identifier. A restore that loses remote identifiers will republish things that are already live, so check that before starting the worker. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Deferred posts per network, the age of the oldest unknown attempt, quota usage against each recorded cap, and the days since each network_limits row was checked. A limits row that has not been rechecked in months is the next outage. ## Incident checklist The incident here is a duplicate live post. Diagnosis starts from publish_attempts, where the unknown outcome and the reconciliation result are recorded, and the fix is almost always that somebody retried an unknown instead of reconciling it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] An over-length or wrongly typed variant is confirmed unschedulable - [ ] Two workers are confirmed to publish a due post once - [ ] A timed-out publish is confirmed to reconcile before retrying - [ ] Quota is confirmed counted in a sliding window - [ ] Metric readings are confirmed append-only ## Launch constraint Do not market omitted FeedHive capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Posts, schedules, attempts and metrics. DATABASE_URL=postgres://feedhive@localhost/feedhive # Required. Originals and cached renditions, keyed by checksum. MEDIA_DIR=./data/media # Required. How long a claim survives a dead worker before the row is claimable again. WORKER_CLAIM_TTL_SECONDS=300 # Optional · secret. Drafting per-network variants. The app works without it and you write the variants yourself. LLM_API_KEY=sk-...redacted # Optional. Drafting stops at this figure for the month and manual editing continues. LLM_MONTHLY_CEILING=3.00 # Required · secret. Per-network OAuth tokens and refresh state. Never in the database dump you share. NETWORK_TOKENS_PATH=./data/tokens.json # Required. How often readings are appended. More often means more rows and a finer history. METRICS_INTERVAL_MINUTES=240
$ choose a build depth, inspect the files, then open the complete pack in your agent
FeedHive: Social tools survive because networks change APIs, permissions, formats, and anti-spam rules constantly.
xanalytics history and attribution
xteam approvals and asset workflows
xprotection from API and policy changes
xcalendar-provider edge cases and timezone correctness
xreliable direct publishing across networks
FeedHive pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| creator | $21.91/workspace | $17.30/workspace | 4 social accounts; 30 scheduled posts; scheduling up to 14 days ahead; 20 days of history; 25 drafts; 15 templates; 500 automations/month; 2,500 base AI credits/month. |
| brand | $33.45/workspace | $25.37/workspace | 10 social accounts; 5 workspaces; 500 scheduled posts; scheduling up to 60 days ahead; 200 days of history; 500 drafts; 50 templates; 2,000 automations/month; 10,000 base AI credits/month. |
| business | $114.19/workspace | $79.58/workspace | 100 social accounts; 50 workspaces; unlimited scheduled posts, history, drafts, and templates; 10,000 automations/month; 50,000 base AI credits/month. |
| agency | $344.87/workspace | $275.66/workspace | 500 social accounts; 100 workspaces; unlimited scheduled posts, history, drafts, and templates; 100,000 automations/month; 100,000 base AI credits/month. |
free tierno free tier (7-day trial only)
billingmonthly + annual (annual saves up to 30%)
hidden costsThe doubled AI-credit allowance is a 3-month launch bonus, not the ongoing quota; public overage-credit prices were not disclosed.
verified 2026-08-14 · source ↗
Vibecode FeedHive
Kinda. The core of FeedHive is buildable in a weekend with the prompt on this page, but there are real gaps: analytics history and attribution, team approvals and asset workflows. Read the honest list above before committing.
How much does FeedHive cost?
FeedHive costs about $33.45/month (Brand, checked 2026-08-14), which is $401.40000000000003 per year.
What do I lose by replacing FeedHive?
Honestly: analytics history and attribution; team approvals and asset workflows; protection from API and policy changes; calendar-provider edge cases and timezone correctness; reliable direct publishing across networks. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to FeedHive?
Yes: Postiz (Open-source social scheduling and publishing platform.), Mixpost (Self-hosted social media management software.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.