Build Ocoya

KINDAback to the verdict

0%0 of 16 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 single-operator content planner whose hard problems are keeping per-network variants derived from one idea without them silently drifting apart, previewing against the constraints a network will actually enforce, and publishing through interfaces that mostly want a publicly reachable media URL rather than your local file.

estimated effort weekendthe files for this build are in the project pack

RuntimeNext.js with TypeScript, run locally or on one small box you ownStoragePostgreSQL for posts and schedule, media on disk with a content hashCompositionOne canonical idea with per-network overrides, never four independent draftsPublishingAn adapter interface with a manual clipboard adapter as the defaultSchedulingA queue table polled by one worker, with the slot stored in a named zone

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 planner and the schedule worker.

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

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Holds ideas, variants and the schedule, and gives the worker the conditional update the queue depends on.

    Get it A local install or a container.

    Verify psql connects and the migration applies cleanly

  3. decidefree

    Why Caption limits, media hosting requirements, container expiry and per-day post caps decide what an adapter can be, and reading them afterwards means rewriting it.

    Get it Read each platform's own developer documentation on content publishing and note the limits and the date.

    Verify Every network in the config carries its limits and a checked-on date

  4. API keyfree to register; some platforms require a reviewed application

    Why Without these the adapter is manual, which is a legitimate answer rather than a gap to paper over.

    Get it Register an application with the platform and complete whatever review it requires.

    Verify One test post reaches the account from your own code

Data model

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

- ideas(id, title, body, created_at, archived_at) · the single source every variant derives from
- variants(id, idea_id, network, caption, override_fields_json, derived_from_revision, edited_at) · derived_from_revision is what detects drift
- media(id, sha256, path, mime, width, height, duration_ms, bytes) · deduplicated by hash, referenced by variants
- slots(id, variant_id, local_time, iana_zone, state, published_at, external_id, failure_reason)
- networks(name, caption_limit, media_required, aspect_ratios_json, alt_text_required, publish_mode) · publish_mode is api, assisted or manual
- publish_attempts(id, slot_id, at, adapter, request_summary, response_code, response_body_excerpt, outcome)
- A variant is never created empty. It is derived from the idea, and the interface shows which fields have been edited away from it.

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_URLrequiredpostgres://localhost:5432/plannerIdeas, variants, slots and attempts.
MEDIA_DIRrequired./data/mediaContent-hashed media files. Included in the export bundle.
MEDIA_RETENTION_DAYSrequired180How long unreferenced media survives before it is removed.
NETWORK_CONFIG_PATHrequired./config/networks.jsonPer-network limits and their checked-on dates.
CONSTRAINT_STALE_DAYSrequired90Above this age the application warns that platform limits need rechecking.
WORKER_POLL_SECONDSrequired30How often the schedule worker looks for due slots.

The build, in order

  1. One idea, variants derived from it

    Writing once produces a per-network draft for every selected network, and edits are visibly local to one variant.

    1. Four independent copies is the bug, not the feature.

      Files src/lib/derive.tstests/derive.test.ts

    2. It is the only way staleness can be computed rather than remembered.

      Files src/db/schema.sql

    3. The edit was deliberate; the overwrite should be too.

      Files src/app/idea/[id]/page.tsx

    done when · tick each as it passes
    watch out
    • Creating four independent drafts at the start and letting the user edit each feels simpler for a day and then produces the classic failure, where the typo is fixed on three networks and lives forever on the fourth. Derive variants from one idea, keep the source revision on each, and make staleness visible rather than something you remember.
  2. Previews that enforce the network's rules, not yours

    A variant that would be rejected is rejected here, with the rule named.

    1. Platform limits change and a constant in the code will not tell you.

      Files config/networks.jsonsrc/lib/constraints.ts

    2. People believe what they can see cut off.

      Files src/components/Preview.tsx

    3. A warning at compose time is a warning nobody acts on.

      Files src/lib/validate.tstests/validate.test.ts

    done when · tick each as it passes
    watch out
    • Hard-coding one caption limit for every network and trusting the editor's character counter produces a draft that looks fine and is truncated mid-sentence on the one network with a shorter limit. Keep constraints per network in data with a checked-on date, validate against them, and show the truncation in the preview rather than a count.
  3. A queue that fires once

    A scheduled slot publishes exactly once, at the wall time you meant, in the zone you meant.

    1. Two workers and one row is the oldest scheduling bug there is.

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

    2. The machine's own clock setting is nobody's intended schedule.

      Files src/lib/schedule.ts

    3. A timeout is not a failure, and retrying it posts twice.

      Files src/worker/publish.ts

    done when · tick each as it passes
    watch out
    • Retrying a slot whose network call timed out is how you post the same thing twice, because a timeout tells you nothing about whether the post was created. Write the attempt before the call, treat an unknown outcome as needing a human, and reconcile against the network before any retry.
  4. Adapters, and being honest about which ones work

    Each network is either genuinely automated or explicitly manual, and the interface never blurs the two.

    1. It is the mode that works everywhere and never lies.

      Files src/adapters/manual.ts

    2. Media hosting, container expiry and post caps shape the adapter.

      Files docs/adapters/README.md

    3. The operator needs to know what will happen at 9am.

      Files src/app/schedule/page.tsx

    done when · tick each as it passes
    watch out
    • Assuming an image upload means posting bytes to an endpoint is wrong for at least one major network: Instagram's content publishing documentation says the platform fetches the media itself, so the file must sit on a publicly reachable server at the moment of the attempt, the container it creates expires if not published within 24 hours, and the account is capped at a published number of API posts per rolling 24 hours. A local-only planner therefore cannot publish there without a public media host, and the honest answer is a manual adapter, not a fake one.
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.