This file is the fast-start context for LLMs and human maintainers working in this repo.
social-agent turns selected Reddit posts into platform-specific social posts, queues them into four daily slots, and publishes them to enabled social platforms.
Current content flow:
- Load tenant-scoped
user_sourcesfrom Supabase. - Accept tenant-scoped source records delivered by the separate Reddit Browser Collector or by controlled fallback imports.
- Bank each accepted tenant-scoped source record into multiple reusable angles.
- Draft only one saved angle at a time into enabled platforms.
- Generate an Instagram image only when Instagram is enabled.
- Copy generated Instagram images into Cloudinary so queued posts use stable delivery URLs.
- Save each transformed item into the automation queue store.
- Publish only to enabled platforms.
- Save publish IDs and history in the automation history store.
config.ts: loads.env, exposes runtime config, and supports per-platform toggles.src/agent.ts: cron scheduler and startup checks.src/cli.ts: local operations likefetch,queue,status, andpost-now.src/server.ts: dashboard/API server onGUI_PORT.src/automation-service.ts: shared API/CLI/cron automation service with readiness gates and SQLite locks.src/supabase-worker.ts: OneClickPostFactory SaaS worker that polls Supabaseagent_jobs, processes jobs byjob.user_id, and writes tenant-scoped results back to Supabase.src/supabase-client.ts: small server-side Supabase REST client that requiresSUPABASE_URL,SUPABASE_SERVICE_ROLE_KEYor aliasesSUPABASE_SECRET_KEY/SERVICE_ROLE_KEY, andCREDENTIAL_ENCRYPTION_KEYfor the worker path.src/tenant-credentials.ts: decrypts SaaSuser_credentials.*_encvalues usingCREDENTIAL_ENCRYPTION_KEY.src/content-engine.ts: shared source-bank / angle-bank / queue orchestration.src/publish.ts: retired shared publish orchestrator. Threads and Instagram adapters fail closed and cannot reach Meta; canonical publication belongs to the workspace deterministic outbox workers.src/runtime-policy.ts: runtime readiness checks, platform readiness, and automation gating.src/control-plane.ts: single-install users, sessions, MFA/RBAC, billing state, runtime settings/secrets, and audit logs.src/validators.ts: API request validation for auth, settings, and queue mutations.src/cloudinary.ts: uploads generated Instagram image assets to stable Cloudinary delivery URLs.src/http-client.ts: shared HTTP helper with timeout/error handling.src/linkedin.ts: LinkedIn publisher using the UGC Posts API.src/x.ts: X/Twitter publisher using OAuth 1.0a or OAuth 2.0 user-context auth depending on configured credentials.src/threads.ts: read-only Threads credential and verification support; publication fails closed withlegacy_meta_publication_disabled.src/instagram.ts: read-only Instagram identity and verification support; publication fails closed withlegacy_meta_publication_disabled.src/facebook.ts: Facebook Group publisher using Graph API feed posts.src/test-meta.ts: Meta diagnostics for identity, Page, Instagram linkage, Group access, and Threads account checks.src/test-x.ts: X diagnostics for the configured auth mode and optional live-post smoke tests.src/store.ts: SQLite-backed queue/history/source/angle/platform-state persistence with one-time legacy JSON import from thedata/directory.src/ai.ts: source extraction, angle drafting, lightweight learning memory, GPT Image generation, and Cloudinary persistence handoff.src/reddit.ts: Reddit fetcher for allowed subreddits.content-os/: repo-ready prompt pack that defines source extraction, platform rules, quality checks, and banned phrasing.
This codebase now has two runtime boundaries:
- Local single-install runtime:
APP_DATA_DIRpoints to one global runtime data directory.- Queue, history, source memory, angle memory, platform state, runtime settings, runtime secrets, and billing state are global for that installation.
- Control-plane users are owner/operator/viewer accounts for the same installation, not separate SaaS customer tenants.
- Billing is represented as one access state for the installation, not per tenant or per workspace.
- OneClickPostFactory SaaS worker runtime:
src/supabase-worker.tspolls Supabaseagent_jobswhenSUPABASE_URL,SUPABASE_SERVICE_ROLE_KEYor aliasesSUPABASE_SECRET_KEY/SERVICE_ROLE_KEY, andCREDENTIAL_ENCRYPTION_KEYare configured.SUPABASE_URLmust point to the owner-managed OneClickPostFactory Supabase project that Lovable also uses; do not point the worker at a hidden managed project whose secret/service-role key and credential encryption key are unavailable.- Every SaaS job is processed by
job.user_id. - SaaS reads/writes for
profiles,user_credentials,user_sources,user_settings,queue_items,publish_history,source_records,angle_records, andworker_logsare scoped byjob.user_id. - SaaS source ingestion also enforces source intent.
user_sourcesrows declareprovider,acquisition_mode,source_scope,target_author,allowed_subreddits, andallow_unfiltered_rss, but the active product direction is no longer Cloudflare Worker Reddit fetching. - There is no active built-in SaaS Reddit scraping path. The active direction is the user-installed OneClick Reddit Connector in
/home/oneclickwebsitedesignfactory/reddit-user-connector; the main app still owns source records, OpenAI angle extraction after valid sources, queueing, publishing, logs, billing, source ownership checks, and the internal owner access override. fetch_sources/refresh_queuemust not treat Reddit public JSON, RSS, OAuth, Browser Run, Playwright browser login, or Devvit as the normal product path. When no usable source records or banked angles exist, the worker should returnreddit_source_ingestion_unavailablewithout calling Reddit, OpenAI, queue creation, or publishing.- Reddit public JSON and Reddit RSS are legacy/quarantined paths. Keep compatibility metadata, fail-closed tests, and historical docs only where needed for old rows or diagnosis. Do not keep active fetch adapters or expose them as normal setup or recovery paths.
- The old per-source Reddit OAuth credential path is not a product path. Reddit OAuth is not available for this project without new confirmed credentials and product approval; do not add tenant Reddit OAuth prompts or present OAuth as the fix.
- Devvit is an archived/reference experiment for source collection because outbound domain approval blocked delivery. Do not present Devvit as an active product path unless a new explicit decision changes that.
- Verified Cloudflare runtime truth for 2026-05-21: the last known good source-fetching flow ran on backend Worker version
3e2a598a-b8bc-461f-9a40-d65b3ad2e156(likely commitfc53fcb Add OpenAI usage visibility and runaway protection) and used adapterreddit_public_json, not RSS. Public JSON returned HTTP 200, fetched 20 subreddit posts, accepted 17, rejected 3 withrejected_author_mismatch, reached OpenAI angle extraction, created angle records, created queue rows, and scheduled publishing later completed. Source rows hadacquisition_mode = oauth, but that label was misleading and has been replaced withpublic_jsonfor Reddit public JSON rows. The old per-source Reddit OAuth path is removed/quarantined; do not rewrite history by claiming RSS or OAuth was the May 21 success path. - RSS evidence is not productive product evidence: HTTP fetches happened, but persisted evidence shows zero accepted posts, source records, angle records, queue rows, publish rows, or OpenAI calls from RSS. Do not make RSS the default or recommend it as a fix.
- Manual Reddit import exists as an advanced fallback, not the main product direction. Existing metadata-only
source_recordsare not draftable shortcuts; queue fill depends on source records with enough source body becomingangle_records. - Before creating any new Reddit connector, service, or runtime, read
docs/CODEX_WORKFLOW_GUARDRAILS.md. - SaaS credential values are decrypted with
CREDENTIAL_ENCRYPTION_KEY. - SaaS billing entitlement is checked from Supabase
profiles; the local SQLite billing state and local billing bypass do not grant SaaS worker entitlement.profiles.dev_access_untilis only a time-limited dev/test override, and it must be presented as dev/test access rather than paid Stripe status. Permanent internal owner access lives in service-role-ownedinternal_access_overrides, keyed by immutable Supabase Auth user id, not email, and must not be represented as paid Stripe status. - Local SQLite remains acceptable for local admin/dev state, but it is not the SaaS tenant source of truth.
Known SaaS worker limitations:
- Facebook Group publishing remains paused until the app/token/group permission path is verified.
This repo is authored in TypeScript with a split execution model: tsx for source-driven dev/operator commands and compiled dist/ output for the production service runtime.
- Edit
.tsfiles, not generated build output. npm run dev,npm run fetch,npm run status, and similar operator commands run from source throughtsx.npm run buildemits compiled output intodist/.npm startandnpm run start:pm2run the compiled service fromdist/src/agent.js.- The build copies
public/andcontent-os/intodist/for the compiled runtime.
As of May 13, 2026:
- Production SaaS publishing uses tenant-encrypted credentials from Supabase, not the old global
.envplatform tokens that were used by the local worker during early testing. - Stored encrypted credential fields only prove that a user saved values. They do not prove the platform token still works. UI and docs must say
Stored, not verifieduntil there is a recent read check, successful publish, or explicit verification result. - Meta/Threads auth failures such as OAuth code
190/Failed to decode, LinkedInINVALID_ACCESS_TOKENor401, and X401 Unauthorized/unauthorized_clientmean the tenant must reconnect that platform. Do not rewrite publisher payloads when read-only token checks already fail. - Failed publish rows should store a user-facing reconnect/payload message in
queue_items.error_messageand log safe diagnostics inworker_logswithout tokens, full headers, or full response bodies. - Threads and Instagram posting are retired from this project. Approved
publications must use
scripts/threads-outbox-runner.mjsorscripts/instagram-publisher-outbox-runner.mjsin the workspace. - LinkedIn code has been merged from
linkedin-agent-v4, but SaaS tenants must provide valid encrypted LinkedIn credentials before live-posting. - X is implemented as a first-class text-only platform. Production SaaS currently uses tenant-scoped OAuth 2.0 user-context credentials, verifies them with
/2/users/me, and logs the safe auth mode asx_oauth2_user_context. - X OAuth 2.0 connect, callback, token persistence, refresh-token support, and portal-token import are implemented in
src/x.ts,src/server.ts, andsrc/cli.ts. - X live-post smoke test succeeded as
@JohnWOE15on April 29, 2026:https://x.com/i/web/status/2049570569494958455. - Threads now uses its own token path through
THREADS_ACCESS_TOKEN. - Threads uses read-only identity and verification endpoints here. This source
tree contains no
/me/threads_publishdispatch. - Facebook/Instagram Graph defaults were bumped to
v25.0. - Instagram can now auto-discover the page-linked
instagram_business_accountand derive a Page access token fromFACEBOOK_PAGE_ID+META_ACCESS_TOKEN. - Instagram generated images must be persisted to Cloudinary before an Instagram queue row is considered publishable; this avoids expiring OpenAI provider URLs in scheduled slots.
- Automation readiness requires Cloudinary configuration when Instagram is enabled. The worker fails closed instead of queueing Instagram rows with temporary image URLs.
- OpenAI image quota or billing hard-limit failures are normalized as
openai_image_billing_blockedwith stageinstagram_image_generation; this is an Instagram media blocker, not a platform credential or publisher payload failure. - Reddit fetch success and OpenAI text angle-extraction failure are separate stages. If accepted Reddit posts are fetched but OpenAI text quota/billing/rate/model access blocks
extractSourceBank, normalize the failure asopenai_text_*, set stageangle_extraction, stop further extraction attempts for systemic failures, and finalize the job. - Accepted source records must be preserved before angle extraction. A
source_recordsrow without matchingangle_recordsis not "done" and must not block future angle extraction after OpenAI billing/quota is fixed. - SaaS jobs must never stay
runningafter terminal OpenAI text extraction failures.agent_jobs.result.summarymust say Reddit succeeded, angle extraction failed, queue rows were not created, and what the next action is. - Cloudflare scheduled automation has three separate stages: enqueue due source fetches, enqueue banked-angle slot-fill jobs, and enqueue due publishes. Scheduled publish only publishes existing
queue_itemswhosescheduled_fortime is due; it must not invent content. Slot filling from existing angles is done by a scheduledrefresh_queuejob withfill_existing_angles_only=true, so it must not fall through into Reddit fetching when no angles are available. - Scheduled slot filling uses existing unused/in-progress, metadata-complete tenant angles and writes normal platform-specific
queue_itemswithscheduled_for. Slot occupancy isuser_id + platform + tenant-local date + slot_index; X slot 0 does not block Threads slot 0. Compute slot times inautomation_timezone, thenposting_timezone, thenEurope/London, and storescheduled_foras UTC. If OpenAI drafting fails while filling a slot, the job must finalize andlast_automation_resultmust explain the OpenAI blocker rather than retrying every minute. - Instagram image generation uses
OPENAI_IMAGE_TIMEOUT_MS, separate from the genericHTTP_TIMEOUT_MS. Keep image timeouts finite, classify aborts asinstagram_image_generation, and skip OpenAI image calls when no Instagram slot is open. - Queue retry behavior is safe: failed platforms no longer delete queued items.
- Partial success is supported: one platform can succeed without forcing the whole slot to fail.
- Source reuse is supported: a Reddit post is only exhausted when no banked angles remain.
- Draft generation skips disabled platforms to save tokens.
- Existing queued items can auto-hydrate missing drafts from stored source and angle memory when a platform is enabled later, including X.
- CLI commands, local API routes, and local cron publishing all go through
src/automation-service.ts, which applies the local automation gate and uses SQLite locks. - The SaaS worker path goes through
src/supabase-worker.ts, uses Supabaseagent_jobs, and does not use local SQLite for tenant queue/history/log/source state. - For SaaS publish regressions, first verify tenant credential decryption and read-only platform identity checks. If a tenant token is invalid but a global
.envtoken still works, the fix is tenant reconnection and clearer error handling, not changing platform endpoint or payload contracts.
Current tracked operational mode in .env.example:
ENABLE_THREADS=trueENABLE_INSTAGRAM=trueENABLE_LINKEDIN=falseENABLE_X=falseENABLE_FACEBOOK=false
Important: config.ts has fallback defaults that enable Threads, Instagram, and Facebook if no env/runtime setting is present. The tracked .env.example pins Facebook off. Keep platform toggles explicit in real deployments.
This was done because:
- Threads and Instagram are both confirmed working.
- LinkedIn has been merged but not yet verified from this repo.
- X remains off in the tracked example config, but live posting is confirmed when valid OAuth 2.0 user-context credentials are configured.
- The Facebook Group still returns
(#3) Missing Permission.
Do not mix these IDs up:
FACEBOOK_USER_ID: the Facebook user account ID returned by/meFACEBOOK_PAGE_ID: the Facebook Page ID returned by/me/accountsINSTAGRAM_ACCOUNT_ID: must be the Page-linkedinstagram_business_accountID, not a Facebook user IDFACEBOOK_PAGE_ACCESS_TOKEN: optional override for Instagram publishing; otherwise the app derives it from the configured PageTHREADS_USER_ID: the Threads account ID returned bygraph.threads.net/meFACEBOOK_GROUP_ID: the Group object ID used for/feed
Do not commit real platform IDs, tenant IDs, account IDs, email addresses, or secrets from .env.
- Production SaaS X publishing uses OAuth 2.0 user-context tenant credentials:
x_client_id_enc,x_client_secret_enc,x_oauth2_access_token_enc, andx_oauth2_refresh_token_enc. - The OAuth2 Client ID must be used exactly as shown in X Developer Console. It may naturally look encoded or decode to a string containing
:1:ci; do not decode it, transform it, or reject it for that reason. Do not use the API Key / Consumer Key in its place. - The production OAuth callback is
https://oneclickpostfactory.woeinvests.workers.dev/auth/x/callback;/auth/x/startmust create state + PKCE and/auth/x/callbackmust exchange the code immediately. - The authorize URL uses the exact decrypted
client_idthroughURLSearchParams; the token and refresh endpoints are the only place base64 is required:Authorization: Basic base64(client_id + ":" + client_secret), with form bodies that do not includeclient_secret. - Supabase stores credential ciphertext, which can look base64-like. Never classify the raw database column as the OAuth client ID, and never infer correctness by base64-decoding the decrypted Client ID.
- Do not add OAuth 1.0a tenant support unless it becomes an explicit product decision. A working old local
.envtoken does not prove a SaaS tenant credential is valid. X_CLIENT_ID,X_CLIENT_SECRET, andX_REDIRECT_URIsupport the OAuth 2.0 user-context connect flow.X_OAUTH2_CLIENT_IDandX_OAUTH2_CLIENT_SECRETare accepted aliases for the labels used in the X developer portal.X_OAUTH2_ACCESS_TOKENandX_OAUTH2_REFRESH_TOKENare supported for v2 posting after OAuth connect.- Do not use app-only bearer tokens for posting.
- X publishing is text-only.
- Auth mode priority in
src/x.tsis OAuth 2.0 refresh-token config, then a static OAuth 2.0 access token, then OAuth 1.0a credentials for local/global runtime compatibility only. - The OAuth 2.0 path authenticates with
/2/users/meand posts through/2/tweets. - SaaS publish jobs must fail cleanly with
needs_reconnectif/2/users/mereturns401 Unauthorizedor refresh returnsunauthorized_client; do not leave X jobs inrunning. - The dashboard owner route
/auth/x/startbegins the OAuth 2.0 flow, and/auth/x/callbackpersists the returned access and refresh tokens into runtime secrets. npm run import-x-oauth2imports user-context OAuth 2.0 access/refresh tokens generated directly in the X developer portal, saves them into encrypted runtime secrets, validates/2/users/me, and only then clears X draft-only mode.- Queued publishing in
src/publish.tsonly attempts X when OAuth 1.0a credentials orX_OAUTH2_ACCESS_TOKENare present. After OAuth 2.0 connect succeeds, the persisted access token satisfies this requirement. - If X returns a credits/access-tier publish entitlement error, the runtime switches X into temporary draft-only mode.
These env vars control which platforms are active:
ENABLE_THREADSENABLE_INSTAGRAMENABLE_LINKEDINENABLE_XENABLE_FACEBOOK
Behavior:
- Disabled platforms are skipped by
src/publish.ts. - A slot is considered complete if every enabled platform succeeds.
- Disabled platforms do not keep a queue item stuck in retry status.
- The automation gate in
src/runtime-policy.tsalso requires an owner account, active billing access, core credentials, and readiness for each enabled platform.
In this workspace, run Node/npm commands inside the Linux shell and load NVM first:
source ~/.nvm/nvm.sh && npm run build
package.json is strict JSON, so script commentary belongs here and in the README command table rather than as inline comments inside the scripts object.
npm run build: compile TypeScript todist/npm run typecheck: TypeScript validation without emitting JSnpm run ci: typecheck, build, compiled smoke test, and local security regression suitenpm run dev: start agent (cron + dashboard) from TypeScript throughtsxnpm run worker:supabase: start only the Supabase SaaS worker loopnpm run test-meta: diagnose Meta setupnpm run test-x: validate the configured X auth mode and optionally live-post a test updatenpm run test: run the local security hardening regression suitenpm run fetch: fill empty queue slots from Redditnpm run queue: inspect queued content and publish IDs/errorsnpm run status: show which slots are fillednpm run memory: inspect source/angle memory countsnpm run history: inspect recent publish historynpm run post-now: immediately post every queued slot to enabled platformsnpm run import-x-oauth2: import X OAuth 2.0 user-context tokens generated in the X developer portalnpm run backup: snapshotAPP_DATA_DIRintobackups/npm run restore -- --from <backup-dir>: restore a backup intoAPP_DATA_DIRnpm run smoke:dist: verify the compiled CLI/runtime wiringnpm start: start compiled agent (cron + dashboard) fromdist/
The repo includes a prompt pack in content-os/:
SYSTEM.mdPLATFORM_RULES.mdQUALITY_CHECKS.mdBANNED_PHRASES.json
src/ai.ts uses this OS in practice by:
- extracting a source summary plus multiple reusable angles first
- drafting natively per platform from one selected angle instead of rewriting line by line
- using lightweight learning notes from recent history to avoid repetition
- checking banned phrases
- scoring specificity, human tone, and platform fit before finalizing
For product direction around drafting quality, strategy profile, and future
learning boundaries, read docs/CONTENT_STRATEGY_ROADMAP.md before changing
angle extraction or drafting prompts.
data/automation.sqlite: queue, history, sources, angles, platform-state, and automation locksdata/control-plane.sqlite: users, sessions, billing, runtime config/secrets, and audit logsdata/control-plane.key: generated local encryption key whenAPP_ENCRYPTION_KEYis not provided; production should useAPP_ENCRYPTION_KEYdata/queue.json,data/used_ids.json,data/history.json,data/sources.json,data/angles.json,data/platform-state.json: legacy import sources retained as runtime artifacts/backups if presentdata/agent.log: dashboard log feedbackups/: default backup output directory fornpm run backup
These files are local runtime state, not source code, and not the source of truth for SaaS tenant data.
- Instagram posting depends on the currently accessible Page continuing to expose the linked
instagram_business_account. - LinkedIn posting still needs a live validation run from this repo even though the publish slice was ported from a working standalone project.
- X posting is confirmed for the current OAuth 2.0 user-context app, but future failures can still happen if credentials expire, the X app permissions change, or credits/access tier are removed.
- Facebook Group posting still depends on app/token/group permissions that are not fixed in code.
- OpenAI image outputs can be temporary or base64-only depending on model/API path. Instagram queue rows must store Cloudinary URLs, not provider URLs.
- Cloudinary must be configured for Instagram queues so generated image assets are copied to a stable delivery URL immediately after generation.
- Compiled output can go stale if
npm run buildis skipped beforenpm startornpm run start:pm2.
- Run
npm run build. - Run
npm run test-meta. - Run
npm run test-xif X is enabled. - Run
npm run statusto see whether X is in draft-only mode. - Run
npm run queue. - Inspect
data/automation.sqlite,data/control-plane.sqlite, anddata/agent.log. - Confirm
.envplatform toggles match intended behavior. - For SaaS worker issues, inspect Supabase
agent_jobsandworker_logsfor the affecteduser_id.
Start here:
src/publish.tsfor platform orchestrationsrc/agent.tsfor cron and startup rulessrc/cli.tsandsrc/server.tsfor manual and API posting behavior
For Meta diagnostics:
src/test-meta.ts
For Threads-specific work:
src/threads.ts
For X-specific work:
src/x.ts