Repository navigation
Releases: XXX1694/steerpost
Release list
v0.5.0
Added
- Sign in with Google or GitHub, API side (D-023; the web screens come next).
GET /auth/providers,GET /auth/oauth/{provider}/start?next=,GET /auth/oauth/{provider}/callback,GET /auth/oauth/pendingandPOST /auth/oauth/complete {display_name, accept_terms}. The callback binds the browser to the flow with a state cookie (HttpOnly, SameSite=Lax, path/api/v1/auth/oauth, 10 min) and requires query state = cookie = an unused row of that provider; the PKCE verifier is stored encrypted with theENCRYPTION_KEYcipher; the redirect URI comes fromAPI_PUBLIC_URL, never fromHost;nextgoes through one shared allow-list (domain/redirect, also used by the account-connect flow). Every callback outcome is a302to the web app (/login?error=<code>on failure). A new user gets no account until they accept the Terms at/signup/complete(D-016); the ticket is redeemed in the same transaction as the user, so a refused form does not burn it. Disabled accounts are refused; an account waiting for deletion can sign in with its provider to cancel the deletion, but never gains a new link by email match, and an automatic link sends the owner anidentity_linkednotice mail. Start, callback and complete sit behind the auth rate limiter, the worker deletes expired flows hourly, and the audit log recordsuser.registered/user.loginwith{method}anduser.identity_linked. No tokens or addresses reach logs or audit metadata. Setup steps for the GitHub OAuth App (and Google, once a domain exists) are indocs/OWNER-SETUP.mdsection 2.0. - Account deletion for users without a password: a session created within the last 10 minutes is accepted instead of a password (the typed email is still required); an older one gets
403 REAUTH_REQUIRED, so deletion stays available and never fails with a 500. Sessions now recordcreated_atfrom the injected clock. GET /api/v1/version(also/version): a public, unauthenticated answer to "what runs in production" without SSH,{"version":"0.4.0","commit":"abc1234","built_at":"2026-10-10T12:00:00Z"}, cacheable for 60 seconds. Release images carry the tag version (amainbuild sayssha-<7 hex>), the short commit and the build time, linked in from build arguments thatrelease.ymlpasses tobackend/Dockerfile; a local build falls back to the Go VCS stamp, thendev/unknown. Nothing about the host or the toolchain is exposed. The Uptime workflow prints the deployed version in its log.- Pause motion in the app (Settings, Preferences), as the landing has had since D-017: it stops every animation and view transition in this browser, on top of the system's reduced-motion setting, and applies before the first paint.
- Manage sign-in methods, API side (D-023; the Settings screen comes next).
GET /auth/identities,POST /auth/identities/{provider}/link(returnsauthorize_url; the provider calls the ordinary sign-in callback, which finishes the link and returns to/settings),DELETE /auth/identities/{provider}(refused with409when it is the last way to sign in) andPOST /auth/password/setfor accounts without a password (session younger than 10 minutes, else403 REAUTH_REQUIRED)./megainsuser.has_passwordanduser.login_methods. Linking and unlinking re-authenticate (current_password, or a session under 10 minutes old for users without a password), are audited and are mailed to the owner. A provider that is switched off does not count as the remaining way to sign in. The demo answers the new routes without pretending to link anything. Failed sign-in and link redirects carryprovider=<id>(and, for sign-in, the sanitisednext). - Sign in with Google or GitHub, web side (D-023).
/loginand/registershare one screen: "Continue with Google / GitHub" buttons on top (only the providersGET /auth/providerslists; the block and its divider are hidden when there are none, and a failed list says so and keeps the email form), then the email form. The buttons are links to/api/v1/auth/oauth/{provider}/start?next=, sonextsurvives the provider round trip. New page/signup/complete: the provider's verified email read-only, the name prefilled and editable, an explicit Terms checkbox (D-016) and "Create account"; with no waiting sign-up (a 404) it goes back to/login?error=signup_expired./login?error=<code>shows plain-language copy for every code the API sends (oauth_cancelled,oauth_state_invalid,oauth_provider_error,email_unverified,account_exists,account_unavailable;identity_in_usegoes to/settings); an optionalprovider=github|googlein that URL names the provider. Deleting the account with a session too old for a password-less user (403 REAUTH_REQUIRED) now offers "Sign in again with {provider}" and returns to Settings; the password field there is optional for people who signed up with a provider. The demo andscripts/mock-api.mjsanswer every new endpoint (GitHub and Google, a simulated redirect to/signup/complete). 28 new strings in every enabled locale andkk(machine drafts). - Manage sign-in methods, web side (D-023). Settings has a "Sign-in methods" section: the password (set or not; "Set a password" for accounts made with Google or GitHub) and each provider the server offers, connected (email and date, Disconnect) or not (Connect). Connect and Disconnect ask for the current password when the account has one, then go to the provider or disconnect; a wrong password shows at the field, the last remaining method is refused with "keep at least one way to sign in", and a session too old for a password-less account (
403 REAUTH_REQUIRED) offers "Sign in again with {provider}" for the providers that account is linked to. A provider the admin switched off keeps only Disconnect./settings?error=<code>&provider=<id>shows plain copy for the codes the link callback sends (anything else reads as a generic sentence and is never echoed) and is cleared from the address; a successful connect carries no flag, so the list is simply read fresh. The delete-account dialog hides the password field for password-less accounts and offers sign-in-again buttons for their linked providers only; the change-password form is hidden for them. The demo has no provider to visit and says so instead of connecting.scripts/mock-api.mjsmodels password-less accounts (an empty password never signs in). Strings in every enabled locale andkk(machine drafts).
Changed
- A
vX.Y.Ztag build no longer pushessha-<7 hex>: that tag is pushed only by themain(or manual) build of the commit, so its digest never changes and deploys pinned to it stay reproducible. A tag build pushesX.Y.Z,X.YandX; a tag that is notvMAJOR.MINOR.PATCH[-pre]now fails the run instead of pushing only the sha tag. - Uptime monitoring is sturdier, and the owner guide sets up an external monitor. GitHub ran the
*/15schedule only every 3 to 7 hours, so a 70-minute outage on 2026-10-09 went unnoticed. The workflow now fires at :07, :22, :37 and :52 (off-peak minutes) and can also be started byrepository_dispatch(uptime-check) or by hand with its ownurls. Each URL gets 4 attempts with 5/10/15 s backoff before it counts as down (was 3). Incident issues carry the labelsincidentanduptime, name the last HTTP code and link the run, and the run summary lists every target. Recovery still comments and closes the issue. OWNER-SETUP section 6 sets up UptimeRobot Free step by step: four monitors (app/api/v1/health, API/api/v1/ready, MCP/health, web/login) with keywords, 5-minute checks, email and Discord alerts, and a public status page. Telegram alerts need UptimeRobot's paid Solo plan or HetrixTools Free; the guide says so. No account is created by the repository. - CI checks first-load JS (issue #180). After the production build,
npm run budget(frontend/scripts/bundle-budget.mjs) sums the gzip (level 6) of each route's scripts from.next/app-build-manifest.json: root main files, the layout, template, error, loading and not-found entries of every level above the page, and the page itself (the same scripts the prerendered HTML lists, polyfills aside) (Next's table omits them), polyfills excluded.frontend/bundle-budget.jsonholds the limits for/login,/register,/verify-email,/dashboard,/compose,/postsand/settings(measured size rounded up plus 2 kB). The job also fails whenskipToContent(shell.json) is back in the root layout chunk or Radix Dialog is in an auth route's chunks. Raising a budget means editing the JSON in the same PR with a reason; lower it when a route shrinks. No new dependency or action. - Each route loads only the message namespaces it uses (step 3 of #145; text and behaviour unchanged). The root layout carries
commonanderrors;(auth)addsauthandlegal;(app)addsnav,shell,languageandlegal; each page adds its own (src/i18n/scopes/*.tsx, mapping indocs/copy/translation-process.md). The demo banner bringsshellin the demo build only. First-load JS (gzip of the page and its layout chunks, from.next/app-build-manifest.json; Next's table leaves the layout chunk out) went from 169.7 to 162.0 kB on/login, 162.0 to 154.3 on/verify-email, 175.7 to 167.7 on/dashboard, 185.4 to 177.9 on/compose, 179.6 to 171.2 on/accounts, 182.4 to 176.1 on/settingsand 173.0 to 164.3 on/posts; the shared baseline stays 104 kB.LegacyMessagesScopeis gone. In development, asking for a namespace no scope above the component loaded logsMISSING_SCOPE: <key>once and renders the key;tests/i18n-route-scopes.test.tsxfails when a page's components use a namespace its scopes do not load, and the site smoke test fails when a demo screen shows a raw key in English or Russian. - Message loading runs through scopes (step 2 of #145; text and behaviour unchanged).
MessagesScope(`src/i18...
v0.4.0
Added
- Sign-in provider adapters for Google (generic OpenID Connect) and GitHub (OAuth 2.0), not wired to any endpoint yet (D-023). Authorization Code with PKCE (S256); the ID token is verified with
coreos/go-oidc(signature, algorithm allow-list, issuer, audience, expiry, authorized party) and our code checks the nonce; GitHub uses the primary address and only when GitHub marks it verified. New settingsGOOGLE_CLIENT_ID/SECRETandGITHUB_CLIENT_ID/SECRET(passed through both compose files): a provider is off while its pair is empty, and half a pair stops the start. New dependencies:coreos/go-oidc/v3and itsgo-jose/v4(Apache-2.0) andgolang.org/x/oauth2(BSD-3-Clause). - Groundwork for social sign-in (D-023; no endpoint yet): migration 00008 adds
user_identitiesandauth_oauth_flowsand makesusers.password_hashnullable, with Postgres repositories and the pure linking functiondomain/identity.Decide(never auto-links an unverified local email). A user without a password cannot sign in with one, the login answer and its timing match a wrong password, and changing a password is refused with a pointer to setting one. - Russian and Kazakh catalogs for the dashboard and the demo (
frontend/messages/ru.json,kk.json, 687 keys each, ICU plurals per CLDR). Machine drafts for native review: neither locale is enabled yet (separate change),docs/copy/review/{ru,kk}.mdlist the terms and strings to check. The post-list row wraps its retry button and fits longer Edit labels at 320 px. - App translations for es, pt-BR, de, fr and id (D-021): complete catalogs in
frontend/messages/<locale>.json(676 keys each, CLDR plural categories, glossary terms), machine drafts with the choices and open questions indocs/copy/review/<locale>.md(section "App"). The locales are not enabled yet, so the language switcher is unchanged. - Japanese (
ja) and Simplified Chinese (zh-CN) catalogs for the dashboard and the demo: all 676 keys infrontend/messages/ja.jsonandzh-CN.json, machine drafts that followdocs/copy/glossary.csvand the landing translations (CJK punctuation,other-only plurals, no "Claude" claim in zh-CN copy). Neither locale is enabled yet (frontend/src/i18n/locales.tsis unchanged), so users still see English until a separate change enables them.docs/copy/review/ja.mdandzh-CN.mdhave an "App" section with the terminology and the strings that need a native check. The audit-log filter buttons wrap on narrow screens. - Landing page in 9 languages (D-021): ru, es, pt-BR, de, fr, id, ja, zh-CN and ar, besides English, at
/{locale}/(English stays at/,/en/forwards to it).site/build.mjsrenders the page fromsite/i18n/landing.<locale>.json(ICU plurals and rich tags throughintl-messageformat), with<html lang dir>, localized title, description and Open Graph tags, hreflang for every locale plusx-default, and the locales in the sitemap. Docs and the demo stay English and are linked with "(EN)". Every translation is a machine draft labelled "Beta translation" in the footer;docs/copy/review/<locale>.mdlists what needs a native look.kkis built hidden (noindex, not linked) until a native reviewer signs it. - A compact language menu in the nav and a plain link list in the footer, both with endonyms, no flags, one line at 320 px. A first-visit suggestion on the English page follows
navigator.languages(same matching as the app,ukandzh-Hantget English); it is a small dismissible card, never a redirect, and the choice is remembered inlocalStorage(steerpost_locale, shared with the demo). - Right-to-left layout for
arthrough logical CSS properties; CJK and Arabic use system fonts with phrase-aware line breaking and no negative tracking; Cyrillic uses Inter's own Cyrillic subsets (fetched only by pages that need them). "Pause motion" and reduced motion behave the same in every language. npm run checkverifies a page, a complete catalog and complete hreflang for every locale;npm run smokeloads each locale at 320, 390 and 1440 px and asserts no horizontal overflow, a one-line nav, the motion switch and the suggestion.- Internationalization infrastructure (D-021; no translated copy yet). A client-side locale provider that works the same in the server build and the static demo (no middleware, no locale routes): the locale is resolved as user setting (later
users.locale) →localStorage(steerpost_locale) →navigator.languages→ English,<html lang>anddirfollow it (right-to-left only forar), anduk,zh-TW,zh-HKandzh-Hantbrowsers get English. Message catalogs live infrontend/messages/<locale>.jsonwithenas the source and empty files for the target locales; a missing key falls back to English. ICU MessageFormat comes from a small in-house runtime (src/i18n/icu.ts, plural, select, ordinal, rich tags) with typed keys; it is checked against FormatJS in tests. next-intl 4.14 supports Next 15 but added about 14 kB gzipped to every route (measured: first-load JS of/114.8 kB → 128.7 kB), so it was not used; the runtime adds about 2 kB (114.8 kB → 116.9 kB, the Next build table stays at 103 kB shared). - Language switcher in Settings → Preferences and a compact one in the sidebar. It lists only enabled locales by their own name and marks machine-drafted ones "Beta translation". Only English is enabled so far; the pseudo-locale
en-XA(accented, about 35 % longer) is listed in dev builds and the demo, to find hard-coded strings and overflow. Intlformatting helpers for numbers, dates, times and relative times that honour the locale and the Settings timezone (src/i18n/format.ts,useFormat()). Arabic keeps Latin digits and the Gregorian calendar.npm run i18n:check(part ofnpm run lint): every catalog key must exist in English, every message must be valid ICU with the same placeholders and tags, enabled locales must be complete with all plural categories, code may only use keys English defines; unused keys and missingmeta.jsondescriptions are warnings.- Every user-visible string of the dashboard and the demo now comes from the English catalog (
frontend/messages/en.json, 674 keys in 17 namespaces (nav,common,errors,shell,auth,dashboard,posts,composer,calendar,accounts,approvals,settings,developer,media,analytics,legalandlanguage). The English text is unchanged. Counts and states are ICUpluralandselectmessages (no more string concatenation or hand-made "s"), dates, numbers, sizes and relative times follow the locale and the Settings time zone, and every key has a note for translators inmessages/meta.json, including the ambiguous ones (Draft, Schedule, Expired, Never, Cancel vs Cancel post vs Keep post, Retry vs Try again, Connect, Requested by, Approved: action, "Post this code there", "{n} min left" and others). npm run i18n:literals(part ofnpm run lint, andtests/i18n-literals.test.ts) fails on hard-coded English in JSX text, inaria-label,title,placeholder,altand the copy props of components, and in string literals that read like UI sentences. The allow-list (frontend/scripts/i18n-literals.allow.json) names a reason for every exception: the Terms and Privacy text, static metadata and the web manifest, the demo seed and engine, API fallback messages.tests/i18n-extract.test.tsxrenders screens inen-XAand fails if plain English is left on screen.<T k="..." />for server components,useErrorText()andnodes()(rich messages with links) insrc/i18n;lib/*functions take the translator as a parameter and stay pure.- The sidebar navigation labels are the first strings read from the catalog.
- Reconnect button on accounts that need reconnecting (OAuth and token networks).
- Glossary: approvals, deny, trusted key, main text, operator, open-source and the approval statuses;
docs/copy/style-guide.mdsection 7 describes server-side approval. - App: a "Skip to content" link as the first tab stop and a focusable
mainlandmark; failed posts on the dashboard get a Retry button; the calendar "+N more" and the mobile post count open that day. - Data export (D-018): Settings, "Your data", builds a ZIP with your profile, posts with targets and attempts, connected accounts (no credentials), API key names, approvals, the audit log and your media files. The worker streams it to storage within its memory cap; the download link works for 5 minutes, the file is deleted after
EXPORT_RETENTION_DAYS(default 7). One export at a time, one per 24 hours. New endpointsPOST/GET /account/exportsandGET /account/exports/{id}(browser session only), migration 00005 (indexes). - Account deletion (D-019): Settings, "Your data", "Delete account". It asks for your password and your email typed out, signs out every session and revokes every API key and MCP connection at once, sets scheduled posts back to drafts, and deletes everything after
ACCOUNT_DELETION_GRACE_DAYS(default 7). Until then you can sign in, see a banner and cancel. The worker removes posts, media (S3 objects too), connected accounts and their stored credentials, keys, approvals and the audit log in batches; only a record of counts without personal data stays. New endpointsPOST /account/deleteandPOST /account/delete/cancel(browser session only),user.deletion_scheduled_atanddeletion_grace_daysin/me, an email when deletion is scheduled, migrations 00006 and 00007. 429 RATE_LIMITEDanswers can carry a specificRetry-After.
Changed
- The dashboard and the demo are now offered in Russian, Spanish, Brazilian Portuguese, German, French, Indonesian, Japanese and Simplified Chinese (D-021): the eight locales are enabled in
frontend/src/i18n/locales.tsand listed by their own names in the language switcher, all labelled "Beta translation" until a native review. Kazakh stays hidden until it is reviewed and A...
v0.3.0
Added
- Steerpost brand identity: new mark, wordmark and lockups (light, dark,
currentColor), favicon and app icons, web manifest, social preview and README banners, a teal palette intokens.csswith the AA test extended to the brand pairs,--ease-steerand--duration-pathmotion tokens, the logo in the app sidebar, and a landing hero whose flow lanes steer through one approval gate. Seedocs/BRAND.md; copy is renamed separately. - Edit drafts and scheduled posts in the web UI. "Edit" on the Posts list and on the post page opens the composer (
/compose?post=<id>) prefilled with the title, text, per-network overrides, media and schedule (shown in the Settings timezone); saving callsPATCH /posts/{id}. A scheduled post keeps its time unless you change it; a draft can be saved and scheduled in one step. Other statuses show why they cannot be edited. Unsaved edits are guarded (browser prompt on reload or close, a dialog on in-app links). The API has no ETag orIf-Match, so before saving the UI re-reads the post and, if itsupdated_ator status moved, shows a conflict message with "Load the latest version" and "Save mine anyway" (best effort: the check and the write are not atomic). - The demo engine and
mock-apianswerPATCH /posts/{id}(text, per-network text, accounts, media, time), so the Pages demo supports editing. - Dashboard "Get started" checklist for new users: connect a network, write a first post, connect an AI agent (an MCP connection or an API key) and, optionally, review an approval. Each step ticks off from existing data (accounts, posts, keys, connections, approvals), has a one-line explanation and a direct link, and the list can be dismissed (remembered in this browser). The empty Accounts, Posts and Developer screens link back to the setup steps. The demo reflects its own state.
Changed
- Rename shims for Steerpost (D-020). The MCP server reads
STEERPOST_API_URL,STEERPOST_API_KEYandSTEERPOST_TIMEOUT_MSfirst and falls back to theSOCIALOS_*names; both compose files set both URL variables. Releases publish the images understeerpost-{backend,mcp,frontend}as well associalos-*(same digest and tags).deploy/README.mdlists the legacy identifiers that keep thesocialosname and the steps to make the new GHCR packages public. autoupdate.shfollows redirects when it asks GitHub for the latest release (a renamed repository answers 301), readsGITHUB_REPOfrom the environment as well as.env, and logs a warning for any answer other than 200 instead of an info line.- Renamed to Steerpost (formerly SocialOS). Product copy, the MCP server name and the generated client config key (
steerpost) changed. Stored and host identifiers keep thesocialosname (cookies, headers, Redis keys,/opt/socialos, systemd units); see "Legacy identifiers" indeploy/README.md. Generated stdio configs keep theSOCIALOS_AUTH_HEADERvariable so configs users already pasted keep working.
v0.2.1
Added
- Agent request limit
QUOTA_AGENT_RPM: requests per minute for all API keys and MCP connections of a user together, answered with429 RATE_LIMITED. Off by default (-1), like the other plan limits (D-014). - MCP tool
get_usage(the 14th, scopeanalytics:read) shows the plan, the period and what is used against each limit. - Settings shows a "Plan & usage" card: the plan, the period and what is used against each limit (from
GET /account/usage), with plain-English copy forQUOTA_EXCEEDED.
Fixed
- Host-proxy mode: MinIO's 80m memory cap made it thrash its page cache (327 GB read from disk in 3 hours, host IO stall, API 503). The default is now 192m with
GOMEMLIMIT=144MiB, andsocialos-guard.shdetects a thrashing container (memory at its cap plus fastworkingset_refault_filegrowth), alerts naming it and restarts it once per cooldown instead of shedding services that cannot help (D-012). The README notes that scheduled GitHub workflows are best-effort and recommends an external uptime monitor.
v0.2.0
Security
- The generated Claude Desktop config no longer runs
npx -y socialos-mcp: that npm package does not exist, and whoever registered the name would have received users' API keys. Configs, the MCP connections page and the docs now use Claude Desktop connectors or themcp-remotebridge pinned to an exact version, and a regression test rejectssocialos-mcpand unpinnednpxpackages.
Added
-
Plan limits (D-014): opt-in limits for one
freeplan: connected accounts, scheduled or published posts per UTC month and media storage, all unlimited by default so updates never cap an existing install. A public instance sets them withQUOTA_ACCOUNTS,QUOTA_POSTS_PER_MONTHandQUOTA_MEDIA_MB;-1switches a limit off. Over a limit the action is refused with403 QUOTA_EXCEEDEDand nothing changes. Drafts are free, a post counts once per month, reconnecting an account you already have is free. Counting is race-free, so parallel requests cannot overshoot. -
GET /account/usage(scopeanalytics:read) shows the plan, the period and what you have used against each limit. -
Dashboard motion: route transitions through the View Transitions API (with a CSS fade-and-rise fallback), a sidebar highlight that slides between items, dialog and toast enter/exit, staggered lists on first load, skeleton shimmer, button press feedback and card hover lift. All motion uses transform and opacity with tokens from
tokens.cssand is disabled underprefers-reduced-motion. Empty states gained an icon and page headers a clearer hierarchy. -
Public pages
/privacyand/terms(no sign-in, linked from the sign-in and register forms and from the app sidebar and Settings), written for a self-hosted, single-operator instance and usable as the LinkedIn app's Privacy Policy URL. They are a template, not legal advice. The operator's name and contact come from the frontend container'sOPERATOR_NAMEandOPERATOR_CONTACT; without them the pages say so. -
Breaking:
POST /auth/registerrequiresaccept_terms: true(otherwise400 VALIDATION_ERRORwithfields.accept_terms). The register form has the matching checkbox. The accepted version and time are stored. Existing accounts are not blocked (D-016). -
A new landing page: full-viewport hero with an animated flow of posts from agent to approval to networks, a real recorded video of the demo (WebM and MP4, light and dark, poster only on phones and with reduced motion), a sticky "agent, approval, network" walkthrough, parallax screenshots, true-fact counters, a marquee of the networks that publish today and an honest list of those that do not. Cross-document View Transitions between the landing page and the docs. Docs keep their own calm layout. See D-017.
-
Email verification, password reset and password change. Mailed links carry a single-use token in the URL fragment (48 h to verify, 30 min to reset). A reset signs out every session; a change keeps the current one. API keys and MCP connections are revoked only if you tick "Also revoke all API keys and MCP connections".
-
When
MAIL_PROVIDER=smtp, unverified accounts get403 EMAIL_NOT_VERIFIEDon connecting networks, scheduling, publishing, editing scheduled posts and creating API keys. With the log provider nothing is restricted and the dashboard says mail is off. Existing accounts start unverified. -
New pages
/verify-email,/forgot-passwordand/reset-password, an email banner, and a Password section in Settings. -
Database migration
00003also prepares plans, quotas, data export and account deletion; it needs no manual step. -
Connect a network with a pasted credential:
POST /api/v1/social/accounts/tokenand the provider capability fieldsconnect_fields,max_image_bytesandrequires_title. It needs the new critical API-key scopesocial:connect, which is never in a default set (D-009). -
"Connect with a token" on the Accounts page: Discord, Mastodon and Bluesky open a form built from the provider's
connect_fields(secrets as password inputs with show/hide, never stored or put in a URL), with the how-to linked and plain-English errors per field. The demo and mock API answer the same fields and the token endpoint. -
Posts are checked against the stricter of the network limits and the account's own limits, and networks that need a title reject posts without one.
-
SSRF-safe HTTP client for hosts users supply (D-010).
-
Discord: connect a channel with its webhook URL and publish text and up to 10 images, or delete a post. The URL is stored encrypted and never returned; a timeout after sending goes to review instead of risking a duplicate. See
docs/integrations/discord.md. -
Connect fields have a
secretflag (connect_fields[].secretinGET /social/providers) for credentials that are not of kindsecret, such as a webhook URL; forms must render them as password inputs. -
Mastodon (and API-compatible Fediverse servers): connect with an instance URL and an access token, publish text and images (public), delete. Limits are read from the instance. A repeated publish after a timeout is safe because of the
Idempotency-Key. Seedocs/integrations/mastodon.md. -
Bluesky: connect with a handle and an app password, publish text with link and hashtag facets and up to 4 images, delete, and resolve unknown outcomes through a deterministic record key. See
docs/integrations/bluesky.md. -
Stub notes for Reddit, Medium and Hashnode; the X note now says the API is pay-per-use.
-
Sharing a host safely (host-proxy mode): every SocialOS container runs in the systemd slice
socialos.slice(1 CPU, 664 MB, no swap, 512 tasks for the whole stack), may not swap and has a process cap; drop-ins cap dockerd and containerd; a guard timer stops the SocialOS worker, then the worker, MCP and UI, only when the host is under pressure and SocialOS is a real contributor (anonymous memory, not page cache, or its own CPU or IO), and resumes them once the host is calm; it also alerts on disk, data budget and the other services' health. A Caddy pre-check keeps a broken SocialOS snippet from stopping the host's Caddy. Runbook:deploy/host-proxy/apply-guardrails.md. -
Approvals for dangerous actions (D-013): publishing now, retrying now, deleting a post, disconnecting an account, connecting with a pasted token, or scheduling less than 5 minutes ahead (
AGENT_MIN_SCHEDULE_LEAD) with an API key or an MCP connection answers428 APPROVAL_REQUIREDand waits for you. Approve or deny in the dashboard under Approvals (GET /approvals,POST /approvals/{id}/approve|deny); the agent repeats the same call withX-Approval-Idand it works once. New settingsAPPROVAL_TTL,APPROVAL_MAX_PENDING(per key),APPROVAL_RETENTION,AGENT_MIN_SCHEDULE_LEAD(also gates edits of posts that run soon); new audit entriesapproval.requested|approved|denied|used. -
API keys have a
dangerous_policy:approve(default, also for existing keys) ortrusted(set when creating the key, skips approvals).
Changed
- Breaking for API-key integrations: dangerous actions made with a key now need your approval (see above). The MCP tools
publish_post,delete_postanddisconnect_accountno longer takeconfirm; they take an optionalapproval_id, andschedule_postandupdate_posttake one too. An agent that only schedules at least 5 minutes ahead is not affected. - Password hashing is bounded in memory: at most
PASSWORD_HASH_CONCURRENCY(default 2) hashes run at once withinPASSWORD_HASH_MEMORY_MIB(default 48) of argon2 memory, new hashes use argon2id m=19 MiB, t=2, p=1 (OWASP), and existing hashes are upgraded at the next successful login. Under a burst, logins and registrations may get429 RATE_LIMITEDand should retry. - The SSRF guard also blocks site-local
fec0::/10, IPv4-compatible::/96and local-use NAT6464:ff9b:1::/48(the whole range is blocked). - Browser sessions now hold ten scopes (the new
social:connect); the "Dangerous" list in the dashboard shows it. - Host-proxy mode: memory caps rebalanced to fit the slice (backend and worker 160m with
GOMEMLIMIT=100MiB, frontend 160m, postgres 112m, minio 80m, mcp 56m, redis 32m, migrate 64m). Automatic updates do not deploy while the guard has shed load. - One shared table for post attempts, analytics, API usage, API keys and the audit log. On phones each row becomes a stacked card showing every column (the Error column is no longer hidden),
/developerno longer scrolls sideways at 390 px, and wide tables can be scrolled with the keyboard.
Fixed
- Large uploads no longer exhaust memory on a small host: media is streamed to object storage in 5 MiB parts (no temp files, no whole-file buffering), the size and type limits are enforced while streaming and the partial object is removed on failure, and at most
MEDIA_UPLOAD_CONCURRENCY(2) uploads run at once, the rest waiting 5 s and then getting429 RATE_LIMITEDwithRetry-After. The web UI uploads directly to the API host, so videos are no longer cut at the Next.js proxy's 10 MB (D-015). - Cancelling, unscheduling or publishing a post while the worker publishes it can no longer deadlock and fail with a 500: the API and the worker now lock rows in the same order (post, then target). A database deadlock or serialization failure that still happens is reported as a retryable
CONFLICT(#38). - Deploying or restarting the worker during a publish no longer sends that post to review: on SIGTERM the worker stops taking tasks, lets in-flight publishes finish (up to
WORKER_SHUTDOWN_TIMEOUT, default 30s, at most 35s; the worker'sstop_grace_periodis now 45s instead of Docker's 10s), stops its background jobs and only then closes the database. The worker's health address is configurable asWORKER_HTTP_ADDR(#39). - The Posts date filter now uses the timezone chosen in Settings instead of the browser's, so "From" and "To" cover whole days where you expect th...
v0.1.0
First release: the MVP, ready to self-host on one server.
Added
- Web dashboard to connect social accounts, write a post once, tailor it per network and publish it now or schedule it.
- REST API with browser sessions (CSRF-protected) and scoped, revocable API keys (
sk_live_…, shown once, stored hashed). - MCP server with 13 tools, filtered by the key's scope, so AI agents can draft, schedule and review posts (Streamable HTTP and stdio).
- Publishing to LinkedIn and Telegram; the other networks are shown honestly as "not available". Connecting a Telegram channel needs a one-time link code.
- Scheduler and worker with idempotent publishing, retries and a clear post state machine; media uploads to S3-compatible storage (bundled MinIO, R2 or S3).
- Production kit in
deploy/: Docker Compose with automatic HTTPS,deploy.sh(migrations, readiness wait, automatic rollback), backups and a runbook. - Host-proxy mode for a small shared server: loopback-only ports, memory limits sized for about 1 GB, and an installer that adds one
importline to the host's Caddy with validation, checks and automatic rollback. - Pull-based updates: a systemd timer deploys the latest GitHub Release when its images exist. It never downgrades and can be switched off with
AUTOUPDATE=false. - Transactional mail through a queue, with SMTP and log-only adapters and templates for e-mail verification, password reset, password changed, account deleted and export ready.
- Audit log of every MCP tool call (tool, route, status, target ids, agent), with an "Agent actions" filter in the dashboard; the MCP server and the backend share a gateway secret so the real client IP is recorded.
- Setup guides for LinkedIn and Telegram, and a project site with an in-browser demo.
Changed
- Dependencies refreshed: Go 1.26, pgx, minio-go, goose and other Go modules, frontend packages, Redis 8; CI actions moved to Node 24 on pinned runners.
- The bundled MinIO now uses a maintained, digest-pinned image because
minio/miniowas removed from Docker Hub. deploy.shfollowsCOMPOSE_FILE, cleans up only SocialOS images on shared hosts, and exits with 75 when it changed nothing.
Fixed
- A scheduled post is never published before its
scheduled_at; an early task is held briefly or handed back to the queue. - The demo's sample data no longer breaks in the first minutes of a month.
Security
- The client address is read from the right of
X-Forwarded-Forand only through trusted proxies (TRUSTED_PROXIES), so a forged header can no longer earn a fresh login or register rate-limit bucket. - Rate limits group IPv6 clients by /64 and track a bounded number of keys.
- Post targets are locked and read per user, with tests that no other user can read, change or lock them.
- A Telegram channel can only be connected by proving ownership with a one-time link code.
- CI scans every change: govulncheck, npm audit, gitleaks over the history, Trivy on the images and CodeQL; the runtime images no longer ship npm.