Build Predis.ai

KINDAback 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 personal social planner whose hard problems are a queue that survives a worker restart without posting twice, the asynchronous upload model the picture networks use, and being honest that a network without a publishing API gets an export file rather than an automation.

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

RuntimeNext.js for the interface and a separate Node worker process, both on PostgresQueueOne scheduled row per target with a unique idempotency key and an atomic claimPublishingOfficial platform APIs only, against accounts you own, with your own credentialsDraftingOne model call per platform variant, stored as a draft a person approvesMediaLocal files validated against each platform's published rules before any uploadExportA dated folder and CSV for every network without a publishing API

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 both the interface and the worker.

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

    Verify node --version prints v22 or higher

  2. installfree locally

    Why The queue relies on a conditional update returning the claimed row and on a unique index, both of which you want on a real database rather than a file.

    Get it Docker, or your package manager.

    Verify A claim query with RETURNING runs and the unique index rejects a duplicate

  3. free, though app review may apply

    Why Most networks publish only from a business or creator account through an app you have registered, and finding that out after building the queue costs a weekend.

    Get it Convert the account in its own settings and register a developer application.

    Verify A test call publishes to the account you intended and nowhere else

  4. have readyfree

    Why Caption lengths, media counts, aspect ratios and publishing quotas all change, and this build stores each one with the date you read it rather than pretending it is permanent.

    Get it Read each platform's own developer documentation and fill platform_rules.

    Verify Every row has a source URL and a checked_on date

  5. decidefree

    Why Deciding this first stops the build drifting towards an unofficial integration for the one network that will not cooperate.

    Get it List your accounts and mark each publishes_via value before writing the worker.

    Verify Every target row has a value and the README lists the export-only ones

Data model

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

- posts(id, created_at, topic, body_master, state) · one idea, before it becomes platform-specific
- variants(id, post_id, platform, body, media_json, char_count, validated_at, validation_errors_json)
- targets(id, platform, account_handle, credential_ref, publishes_via, added_at) · publishes_via is api or export, and there is no third value
- schedule(id, variant_id, target_id, run_at, claimed_at, claimed_by, attempts, idempotency_key, remote_id, state, last_error)
- containers(id, schedule_id, platform_container_id, status_code, poll_count, first_polled_at, last_polled_at) · the asynchronous upload state for the platforms that work that way
- quotas(platform, account_handle, window_start, used, limit_value, checked_at) · read from the platform's own quota endpoint, never assumed from documentation you read once
- platform_rules(platform, field, value, source_url, checked_on) · every character limit, aspect ratio and file type in one table with the date you last confirmed it
- There is one unique index on (target_id, idempotency_key). It is the only thing standing between a worker restart and a duplicate post.

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://localhost:5432/plannerPosts, variants, schedule and quota. The queue lives here.
MEDIA_DIRrequired./data/mediaYour own images and video. Exported alongside the CSV, not separately.
MODEL_API_KEYsecretoptionalsk-...redactedDraft generation only. Without it the editor still works and you write the copy.
PLATFORM_CREDENTIALS_PATHsecretrequired./data/credentials.encEncrypted tokens with their expiry. Never in the database dump you share.
CREDENTIAL_ENC_KEYsecretrequiredbase64:...redactedEncrypts the credential file. Losing it means re-authorising every account.
EXPIRY_WARN_DAYSrequired7How far ahead a dying token warns. Shorter than your longest weekend away.
WORKER_CONCURRENCYrequired2Publishing workers. The claim query is what makes more than one safe.

The build, in order

  1. One idea, several variants, real limits

    A master body becomes per-platform variants that are validated against limits you can point at a source for.

    1. A number in the code is a number nobody will ever recheck.

      Files src/db/platform-rules.tstests/no-hardcoded-limits.test.ts

    2. The worker is the worst place to discover a caption is too long.

      Files src/app/compose/page.tsxsrc/lib/validate.ts

    3. One call per variant, and the output lands in a field a person edits.

      Files src/lib/draft.ts

    done when · tick each as it passes
    watch out
    • Counting a caption with the length of the string gives a different answer from the platform for emoji, combining characters and links, so a variant that looks fine locally is rejected at publish time when the slot has already been booked and the window has passed; put every limit in the platform_rules table with the source URL and the date you checked it, count the way that platform documents, and fail the variant in the editor rather than in the worker.
  2. A queue that cannot post twice

    Killing the worker at any point in the publish path never produces two posts.

    1. Two workers and one row is the whole bug.

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

    2. The database refuses the second post even when the code tries.

      Files src/db/schema.sql

    3. Hand it to a person with the transcript.

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

    done when · tick each as it passes
    watch out
    • A worker killed after the platform accepted the post but before the row was marked done will, on restart, publish the same thing again, and selecting due rows then updating them lets two workers claim the same row in the first place; claim with one UPDATE that both filters and sets the claim, and when a retry finds an attempt whose outcome is unknown, do not guess, move the row to a needs-attention state and tell the person rather than posting again.
  3. The networks that upload before they publish

    An asynchronous upload is polled to completion and a publish never fires blind.

    1. The platform tells you to; a blind publish burns an attempt.

      Files src/worker/containers.tstests/container-poll.test.ts

    2. Posts made from the phone count against the same window.

      Files src/lib/quota.ts

    3. A container that never finishes is a failure, not a loop.

      Files src/worker/containers.ts

    done when · tick each as it passes
    watch out
    • Publishing straight after creating a container fails, because the media is still being processed, and the platform documentation for this model tells you to query the container's status field before publishing and recommends polling about once a minute for no more than five minutes; worse, the account is capped at a number of API-published posts in a rolling twenty-four hour window, so a blind publish that fails can still cost you a slot, which is why you read the quota endpoint rather than counting your own posts.
  4. Approval, and the networks with no door

    Nothing generated publishes without a human approving it, and the platforms without an API get an export instead of a pretence.

    1. No configuration flag may skip it.

      Files src/app/review/page.tsxsrc/db/schema.sql

    2. The first line is the whole post on most timelines.

      Files src/components/Preview.tsx

    3. A folder and a reminder beats a fake integration.

      Files src/lib/export-bundle.ts

    done when · tick each as it passes
    watch out
    • Building a path that lets a generated caption publish unattended feels like the point of the product and is the fastest way to have an account restricted, because the same model produces the same phrasings across a run and the networks are looking for exactly that; make approval a state the row must pass through, with no setting that skips it, and keep the model's original text beside the edited text so you can see what you actually changed.
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.