Build Ocoya
KINDAback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URL | required | postgres://localhost:5432/planner | Ideas, variants, slots and attempts. |
MEDIA_DIR | required | ./data/media | Content-hashed media files. Included in the export bundle. |
MEDIA_RETENTION_DAYS | required | 180 | How long unreferenced media survives before it is removed. |
NETWORK_CONFIG_PATH | required | ./config/networks.json | Per-network limits and their checked-on dates. |
CONSTRAINT_STALE_DAYS | required | 90 | Above this age the application warns that platform limits need rechecking. |
WORKER_POLL_SECONDS | required | 30 | How often the schedule worker looks for due slots. |
The build, in order
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.
Four independent copies is the bug, not the feature.
Files
src/lib/derive.tstests/derive.test.tsIt is the only way staleness can be computed rather than remembered.
Files
src/db/schema.sqlThe edit was deliberate; the overwrite should be too.
Files
src/app/idea/[id]/page.tsx
done when · tick each as it passeswatch 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.
Previews that enforce the network's rules, not yours
A variant that would be rejected is rejected here, with the rule named.
Platform limits change and a constant in the code will not tell you.
Files
config/networks.jsonsrc/lib/constraints.tsPeople believe what they can see cut off.
Files
src/components/Preview.tsxA warning at compose time is a warning nobody acts on.
Files
src/lib/validate.tstests/validate.test.ts
done when · tick each as it passeswatch 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.
A queue that fires once
A scheduled slot publishes exactly once, at the wall time you meant, in the zone you meant.
Two workers and one row is the oldest scheduling bug there is.
Files
src/worker/claim.tstests/claim.test.tsThe machine's own clock setting is nobody's intended schedule.
Files
src/lib/schedule.tsA timeout is not a failure, and retrying it posts twice.
Files
src/worker/publish.ts
done when · tick each as it passeswatch 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.
Adapters, and being honest about which ones work
Each network is either genuinely automated or explicitly manual, and the interface never blurs the two.
It is the mode that works everywhere and never lies.
Files
src/adapters/manual.tsMedia hosting, container expiry and post caps shape the adapter.
Files
docs/adapters/README.mdThe operator needs to know what will happen at 9am.
Files
src/app/schedule/page.tsx
done when · tick each as it passeswatch 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.
Getting your content back outproduct builder
Everything you wrote is exportable, media has a lifecycle, and tokens fail safely.
A bundle without files is a bundle that restores broken.
Files
src/export/bundle.tstests/bundle.test.tsVideo is the only thing here that will fill a disk.
Files
src/worker/retention.tsA revoked token should cost you a copy and paste, not a week.
Files
src/adapters/registry.ts
done when · tick each as it passeswatch out- An export that writes captions but leaves media as database ids produces a bundle that looks complete and restores into a planner full of broken image references. Export the files alongside the JSON, key them by hash, and prove it by importing into an empty database and rendering a preview.
That is the whole plan for Ocoya. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Generated imagery and video.
- Follower and engagement analytics.
- Team approvals and roles.
- Any publishing route outside a platform's published rules.
- protection from API and policy changes
- calendar-provider edge cases and timezone correctness
- reliable direct publishing across networks
- analytics history and attribution
- team approvals and asset workflows
- A public media host you control, which is what would turn one of the manual adapters into an API adapter
- A per-network best-time suggestion computed only from your own published history, with the sample size shown
- A link checker that opens every URL in a scheduled caption before the slot is due
Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.