Build FeedHive

KINDAreplaces $33.45/mosaves $401.4/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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 weekendthe files for this build are in the project pack

RuntimeNode 22, a small server-rendered editor and one worker processStoragePostgreSQL for rows, media files on disk beside it, checksummedPublishingOfficial platform APIs only, one adapter per networkSchedulingDue rows claimed by conditional update, with a per-attempt request identifierLimitsA limits table you fill in from the documentation, with the date you read it

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the editor and the worker.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. installfree 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

  3. 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

  4. installfree

    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

  5. decidefree

    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

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- 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

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://feedhive@localhost/feedhivePosts, schedules, attempts and metrics.
MEDIA_DIRrequired./data/mediaOriginals and cached renditions, keyed by checksum.
WORKER_CLAIM_TTL_SECONDSrequired300How long a claim survives a dead worker before the row is claimable again.
LLM_API_KEYsecretoptionalsk-...redactedDrafting per-network variants. The app works without it and you write the variants yourself.
LLM_MONTHLY_CEILINGoptional3.00Drafting stops at this figure for the month and manual editing continues.
NETWORK_TOKENS_PATHsecretrequired./data/tokens.jsonPer-network OAuth tokens and refresh state. Never in the database dump you share.
METRICS_INTERVAL_MINUTESrequired240How often readings are appended. More often means more rows and a finer history.

The build, in order

  1. Variants that are checked before they are scheduled

    A variant cannot enter the queue until it passes the limits recorded for its network.

    1. With the date, because these numbers move and stale ones fail at publish time.

      Files migrations/002_network_limits.sqldocs/limits.md

    2. String length is not what any of these networks measure.

      Files src/adapters/*/count.tstests/counting.test.ts

    3. Validation at publish time is validation that happens while you are asleep.

      Files src/schedule/create.tstests/validate-gate.test.ts

    done when · tick each as it passes
    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.
  2. A worker that publishes exactly once

    A due post is published once even when two workers run and the API times out.

    1. The row count from the update is the lock.

      Files src/worker/claim.tstests/double-claim.test.ts

    2. So an unknown outcome is investigable rather than guessed at.

      Files src/worker/publish.ts

    3. Look for the post before you make a second one.

      Files src/worker/reconcile.tstests/unknown-publish.test.ts

    done when · tick each as it passes
    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.
  3. Living inside the published caps

    The queue spends against each network's published cap deliberately rather than discovering it through errors.

    1. The documented windows are moving, not calendar days.

      Files src/limits/window.tstests/sliding-window.test.ts

    2. A deferred post is recoverable; a rejected one needs a human.

      Files src/worker/claim.ts

    3. A container has a published lifetime and it is shorter than a weekend.

      Files src/adapters/instagram/container.ts

    done when · tick each as it passes
    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.
  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.

    1. The checksum is the identity of the file, not the filename.

      Files src/media/import.tsmigrations/003_media.sql

    2. Prepared once, uploaded once, reused on retry.

      Files src/media/render.tstests/rendition-cache.test.ts

    3. Re-encoding from a rendition is how quality quietly dies.

      Files src/media/purge.ts

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.