<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lukasz</title>
    <description>The latest articles on DEV Community by Lukasz (@lukasz_141142).</description>
    <link>https://dev.to/lukasz_141142</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078848%2F7f06f82e-db0e-4329-b017-653cc9142144.png</url>
      <title>DEV Community: Lukasz</title>
      <link>https://dev.to/lukasz_141142</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lukasz_141142"/>
    <language>en</language>
    <item>
      <title>How to schedule social media posts from Claude Code with MCP</title>
      <dc:creator>Lukasz</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:35:23 +0000</pubDate>
      <link>https://dev.to/lukasz_141142/how-to-schedule-social-media-posts-from-claude-code-with-mcp-11d1</link>
      <guid>https://dev.to/lukasz_141142/how-to-schedule-social-media-posts-from-claude-code-with-mcp-11d1</guid>
      <description>&lt;p&gt;Claude Code can schedule a social media post when an MCP server exposes the necessary publishing tools. You connect the server, authenticate, and ask Claude to work with a real account rather than return a caption for you to copy elsewhere.&lt;br&gt;
The part to design carefully is the write boundary. A request to prepare content should not quietly become permission to schedule it. And a message saying "done" should not replace a check of the saved post.&lt;br&gt;
This tutorial uses one text-only LinkedIn draft to demonstrate a controlled workflow. The server in the example is PostSider's hosted MCP service. I build PostSider; the command and argument examples below describe the integration, not an authenticated publishing session executed for this article.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connect the hosted MCP server
&lt;/h2&gt;

&lt;p&gt;You need Claude Code installed and a publishing account with the intended social media channel already connected.&lt;br&gt;
Add the remote server from your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http postsider https://mcp.postsider.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This connection uses Streamable HTTP. You do not need to launch a local PostSider MCP process.&lt;br&gt;
Open Claude Code and enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select the server and follow its OAuth sign-in flow. Review the organization and permissions before granting access. The workflow needs read access to inspect posts and write access to create and schedule them.&lt;br&gt;
Do not paste an API key into the conversation or append it to the endpoint URL. The hosted route uses OAuth credentials handled by the client.&lt;br&gt;
Keep normal write-confirmation prompts enabled. Do not bypass permission checks just to make the first test faster.&lt;/p&gt;
&lt;h2&gt;
  
  
  Make the first request read-only
&lt;/h2&gt;

&lt;p&gt;Start by asking Claude to identify the available accounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List my connected PostSider channels.
Do not create or modify anything.
Show each account's name, platform, and ID so I can select one.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relevant tool is &lt;code&gt;postsider_list_channels&lt;/code&gt;. Select the intended account from the returned data instead of guessing an ID or assuming that the first LinkedIn result is correct.&lt;br&gt;
This is especially important when you manage a personal profile and a company page, or several client accounts.&lt;br&gt;
Ask Claude to check the organization's publishing state with &lt;code&gt;postsider_get_publishing_state&lt;/code&gt;. If publishing is paused, stop rather than attempting to work around it.&lt;br&gt;
You can also inspect the target date range with &lt;code&gt;postsider_list_posts&lt;/code&gt; before preparing a new post. A calendar read helps reveal an existing announcement, although it is not a lock against concurrent writes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Specify the content and time before creating a draft
&lt;/h2&gt;

&lt;p&gt;Use one account and one piece of approved text for the first run. Media uploads and multiple destinations introduce additional requirements that can wait.&lt;br&gt;
Give Claude the exact content and an explicit future time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prepare one text-only LinkedIn draft for the account I selected.
Copy:
Before scheduling your next social media post, check the destination
account and the timezone.
Target time:
November 16, 2026, at 14:00 UTC.
Show the resolved channel ID, exact copy, and UTC timestamp.
Ask for permission before creating the draft.
Do not schedule or publish anything yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the date with a future time when you use this example. If you specify a local time, include an IANA timezone such as &lt;code&gt;America/New_York&lt;/code&gt; and ask Claude to show the UTC conversion for review.&lt;br&gt;
Avoid "tomorrow morning." The assistant would have to choose both the time and its timezone interpretation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Set the creation action explicitly
&lt;/h2&gt;

&lt;p&gt;In this server's current schema, &lt;code&gt;postsider_create_post&lt;/code&gt; defaults to &lt;code&gt;schedule&lt;/code&gt;. Creating a draft therefore requires an explicit &lt;code&gt;type: "draft"&lt;/code&gt;.&lt;br&gt;
Drafts also require a date in the current contract.&lt;br&gt;
Once you authorize draft creation, this is an illustrative argument shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-16T14:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shortLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"posts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"channelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CHANNEL_ID_FROM_LIST_CHANNELS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Before scheduling your next social media post, check the destination account and the timezone."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotencyKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"linkedin-draft-2026-11-16-example-001"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This JSON is for the MCP tool, not the REST endpoint. Ask Claude to inspect the connected server's live tool schema before calling it.&lt;br&gt;
Replace the channel placeholder with the selected account ID. Choose a unique idempotency key for the operation and preserve it for unchanged retries. Do not reuse this example key for separate posts.&lt;br&gt;
The empty settings object is used for this simple LinkedIn example. It should not be copied indiscriminately to other networks, which may require additional fields or media.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read the draft back before approving its schedule
&lt;/h2&gt;

&lt;p&gt;After creation, retain the real post ID returned by the tool. Ask Claude to call &lt;code&gt;postsider_get_post&lt;/code&gt; using that ID.&lt;br&gt;
The response includes a &lt;code&gt;posts&lt;/code&gt; array. Find the record whose &lt;code&gt;id&lt;/code&gt; matches the returned post ID and compare its saved content, destination, and &lt;code&gt;publishDate&lt;/code&gt; with the proposal. Confirm that it is a draft.&lt;br&gt;
Inspect the platform preview in the dashboard too, especially when you later add links or media.&lt;br&gt;
Do not assume that a tool with "validation" or "missing fields" in its description performs every necessary check. In this implementation, &lt;code&gt;postsider_get_post_missing_fields&lt;/code&gt; is a narrow missing-content diagnostic. An empty result does not prove that the eventual publication will succeed.&lt;br&gt;
The &lt;a href="https://postsider.com/blog/how-to-schedule-social-media-posts-with-mcp/" rel="noopener noreferrer"&gt;full MCP scheduling walkthrough&lt;/a&gt; explains that limitation and the complete tool sequence.&lt;/p&gt;
&lt;h2&gt;
  
  
  Confirm scheduling as a separate action
&lt;/h2&gt;

&lt;p&gt;Once the saved draft is correct, give explicit permission to schedule that particular post.&lt;br&gt;
The confirmation should identify the saved post ID and the exact account. Repeat the intended timestamp so that the action does not depend on an earlier, ambiguous message.&lt;br&gt;
For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Schedule the saved draft with ID [actual post ID] to [exact account name]
at the confirmed UTC timestamp.
Keep the saved content unchanged. Do not modify other posts.
Before changing status, verify that the date is still in the future
and that organization publishing is not paused.
After scheduling, read the same post ID again and show the saved result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status-change tool is &lt;code&gt;postsider_update_post_status&lt;/code&gt;. Its arguments look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST_ID_FROM_CREATION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"schedule"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This operation changes status; it does not accept a replacement date. If the draft's saved date needs correction, stop and review the change in the dashboard before scheduling.&lt;br&gt;
Chat confirmation is not a substitute for an organization's required approval workflow. Keep those requirements separate from the assistant's instructions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verify the record instead of trusting the completion message
&lt;/h2&gt;

&lt;p&gt;After the status change, Claude should retrieve the same post again.&lt;br&gt;
A useful completion report includes the real post ID, selected account, saved text, publishing timestamp, and state. Ask it to report any mismatch or error instead of summarizing the operation as successful.&lt;br&gt;
A &lt;code&gt;QUEUE&lt;/code&gt; state means scheduled. It does not mean the social network has already published the content.&lt;br&gt;
Check again after the publication time. Account authorization or platform requirements can still cause a failure. Inspect the published URL when one is available.&lt;/p&gt;
&lt;h2&gt;
  
  
  Recover from a timeout without creating another post
&lt;/h2&gt;

&lt;p&gt;A creation timeout does not establish whether the write succeeded.&lt;br&gt;
If Claude retries the same creation operation, it should keep the original idempotency key and unchanged arguments. A new key can turn recovery into another creation request.&lt;br&gt;
If the outcome remains unclear, stop and inspect the calendar. Do not ask the assistant to invent an ID or create a replacement because it cannot find a response.&lt;br&gt;
This is why retaining the returned post ID matters: later checks can target a specific record rather than search by caption and hope it is the right one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reuse a prompt with clear write boundaries
&lt;/h2&gt;

&lt;p&gt;After the first run, you can reuse this prompt with your own values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prepare one text-only social media post.
Account: [platform and exact account name]
Copy: [approved text]
Target time: [future date, time, and timezone]
1. List channels and resolve the intended account ID.
2. Check publishing state and inspect the target date range.
3. Inspect the live tool schemas.
4. Show the exact proposal before any write.
5. Ask permission to create one draft with type=draft and a unique,
   stable idempotency key.
6. Read the created draft by its returned ID. Compare saved content,
   destination, timestamp, and draft state. Check provider requirements.
7. Ask separate confirmation before scheduling that draft.
8. After confirmation, recheck publishing state and the future date,
   schedule that saved post, and retrieve the same ID again.
9. Report the saved result and any errors.
Do not publish immediately, alter other posts, bypass required team
approvals, or create a duplicate to recover from a timeout.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not guarantee that an agent will obey every instruction. It makes the intended sequence explicit and gives you identifiable points at which to check its actions.&lt;br&gt;
Start with one post you can follow from draft to publication. Expand the workflow only after you can verify what the assistant changed.&lt;br&gt;
&lt;em&gt;Disclosure: I am the founder of PostSider. This article was prepared with AI assistance. No authenticated social media scheduling was performed for this article; verify the live schemas and your own results before relying on the workflow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>claude</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to automate social media publishing with n8n or Make</title>
      <dc:creator>Lukasz</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:28:04 +0000</pubDate>
      <link>https://dev.to/lukasz_141142/how-to-automate-social-media-publishing-with-n8n-or-make-183g</link>
      <guid>https://dev.to/lukasz_141142/how-to-automate-social-media-publishing-with-n8n-or-make-183g</guid>
      <description>&lt;p&gt;A CMS event or spreadsheet row can become a scheduled social media post through an HTTP request. The harder part is making sure a repeated execution does not create another copy, and that the post reaches the account and time you intended.&lt;br&gt;
This walkthrough describes a small publishing pipeline you can build in n8n or Make. It starts with approved content, creates a scheduled post through an API, and stores enough information to recover from failures.&lt;br&gt;
The request example uses PostSider, which I build. The workflow described here has not been executed inside n8n or Make for this article.&lt;/p&gt;
&lt;h2&gt;
  
  
  Choose a source event, not just a timer
&lt;/h2&gt;

&lt;p&gt;Start with something that makes content ready for distribution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CMS article changes to published.&lt;/li&gt;
&lt;li&gt;A content-calendar row receives approval.&lt;/li&gt;
&lt;li&gt;A release announcement becomes available.&lt;/li&gt;
&lt;li&gt;An internal application emits an approved update.
A timer can check for those events, but running the workflow should not itself authorize publication.
For example, a spreadsheet-driven workflow can select records where:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;approval_status = approved
publishing_job_id is empty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That selection is a starting point, not a complete duplicate-prevention mechanism. Two overlapping executions could still select the same row.&lt;br&gt;
If your platform allows concurrent runs, add a claim or lock around the source record. Use API idempotency as a separate recovery mechanism.&lt;/p&gt;
&lt;h2&gt;
  
  
  Define the publishing record first
&lt;/h2&gt;

&lt;p&gt;Keep the source data separate from the API request.&lt;br&gt;
An illustrative source record might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"record_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-announcement-042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"revision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"approval_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONNECTED_ACCOUNT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The approved announcement."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publish_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-16T14:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-announcement-042-r1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publishing_job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are your workflow fields, not a publishing API schema.&lt;br&gt;
The record should identify a specific account. A platform name such as "LinkedIn" is insufficient when you manage several profiles or pages.&lt;br&gt;
Store the intended publishing time before making the request. Choose a future timestamp and convert it from an explicit timezone. Do not recalculate it on each retry.&lt;br&gt;
If the approved content changes, treat that as a reviewed revision rather than silently changing a request already in flight.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use a read request to verify authentication and destination
&lt;/h2&gt;

&lt;p&gt;Before creating anything, list the connected accounts.&lt;br&gt;
For the API used in this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET https://api.postsider.com/public/v1/integrations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Organization API keys use the raw key in the &lt;code&gt;Authorization&lt;/code&gt; header, without a &lt;code&gt;Bearer&lt;/code&gt; prefix.&lt;br&gt;
Keep the key in credential storage. It should not appear in the source spreadsheet, exported workflow, or public example.&lt;br&gt;
Inspect the returned accounts and select the intended channel ID. Do not automatically choose the first result.&lt;br&gt;
A successful account-list request confirms that authentication works. It does not prove that every publishing request will be accepted.&lt;/p&gt;
&lt;h2&gt;
  
  
  Build the workflow in n8n
&lt;/h2&gt;

&lt;p&gt;Use an HTTP Request node for the API call.&lt;br&gt;
A first version can follow this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Manual Trigger
  -&amp;gt; Read one source record
  -&amp;gt; Check approval and required fields
  -&amp;gt; Persist the request and idempotency key
  -&amp;gt; HTTP Request: create scheduled post
  -&amp;gt; Store returned post ID
  -&amp;gt; HTTP Request: read post
  -&amp;gt; Compare saved fields
  -&amp;gt; Update source status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For organization-key authentication, configure a generic Header Auth credential with &lt;code&gt;Authorization&lt;/code&gt; as the header name.&lt;br&gt;
The creation node uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Method: POST
URL: https://api.postsider.com/public/v1/posts
Body content type: JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Map the caption and timestamp from the prepared record. Preserve booleans and arrays as their actual JSON types.&lt;br&gt;
Start with a manual trigger and one record. Enable recurring execution only after you can inspect the created post and reconcile it with the source.&lt;/p&gt;
&lt;h2&gt;
  
  
  Build the equivalent scenario in Make
&lt;/h2&gt;

&lt;p&gt;Use the HTTP app's &lt;strong&gt;Make a request&lt;/strong&gt; module.&lt;br&gt;
Configure API-key credentials to send the key in the &lt;code&gt;Authorization&lt;/code&gt; header. Select &lt;code&gt;application/json&lt;/code&gt; for the body content type and enable response parsing.&lt;br&gt;
The request contract is the same as in n8n. The difference is how values are mapped between modules.&lt;br&gt;
Use a JSON data structure for captions containing quotation marks or line breaks. Manually inserting those captions into a raw JSON string can produce invalid JSON.&lt;br&gt;
Put an approval filter before the creation module. After creation, write the returned post ID to the source record rather than relying only on the scenario's execution history.&lt;br&gt;
The module documentation is available in the &lt;a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/" rel="noopener noreferrer"&gt;n8n HTTP Request reference&lt;/a&gt; and &lt;a href="https://apps.make.com/http" rel="noopener noreferrer"&gt;Make HTTP reference&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Send the platform-specific request
&lt;/h2&gt;

&lt;p&gt;Here is an illustrative payload for one text-only post to a connected X account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"schedule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-11-16T14:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shortLink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"posts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"integration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_X_CHANNEL_ID"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your approved announcement goes here."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"__type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"who_can_reply_post"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"everyone"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the channel ID and choose a future date before using it.&lt;br&gt;
Add an &lt;code&gt;Idempotency-Key&lt;/code&gt; header using the key already stored with your source record.&lt;br&gt;
This payload is specific to X. Do not adapt it to another network by changing only the account ID. Different platforms can require different settings or media.&lt;br&gt;
The &lt;a href="https://postsider.com/blog/how-to-schedule-social-media-posts-with-an-api/" rel="noopener noreferrer"&gt;full API scheduling walkthrough&lt;/a&gt; explains the request fields, authentication, and readback behavior behind this example.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read the created post before updating the source
&lt;/h2&gt;

&lt;p&gt;The creation response returns an array containing post IDs and integration identifiers.&lt;br&gt;
For this single-destination example, retain the returned &lt;code&gt;postId&lt;/code&gt;, then request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET https://api.postsider.com/public/v1/posts/RETURNED_POST_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail response contains a &lt;code&gt;posts&lt;/code&gt; array. Find the record matching that ID and compare its content, destination, and &lt;code&gt;publishDate&lt;/code&gt; with the prepared request.&lt;br&gt;
Confirm its scheduled state before marking the source record as scheduled.&lt;br&gt;
Keep "scheduled" separate from "published." A &lt;code&gt;QUEUE&lt;/code&gt; state means the scheduler has work to perform. Publication can still fail later because of account authorization or platform requirements.&lt;br&gt;
After the publishing time, check the post again. When a publication URL is available, inspect the destination result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make retries recover the original operation
&lt;/h2&gt;

&lt;p&gt;A timeout is ambiguous. The service may have created the post even if your workflow did not receive the response.&lt;br&gt;
Retry an unchanged creation request with its original idempotency key. Keep the JSON body unchanged too, including the timestamp and property order.&lt;br&gt;
Generating a new key during recovery defeats that protection.&lt;br&gt;
Handle failures according to their meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timeout or transient server error:&lt;/strong&gt; Retry with the saved key and payload, using a bounded retry policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit response:&lt;/strong&gt; Respect the supplied retry interval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation error:&lt;/strong&gt; Stop and inspect the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication failure:&lt;/strong&gt; Repair credentials or account access before retrying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict:&lt;/strong&gt; Read the error; do not assume it means the post is safe to recreate.
If the outcome remains unclear, reconcile the source record with the publishing calendar before making another write.
Idempotency reduces duplicate creation. It does not guarantee exactly-once publication across social networks.
## Keep enough evidence to troubleshoot
For each publishing operation, retain:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source record ID and revision
Prepared request
Idempotency key
Returned post ID
Last verified state
Error details, if any
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid logging the authentication header.&lt;br&gt;
This gives the person investigating a failure a way to identify the operation without guessing from the caption.&lt;br&gt;
For multiple destinations, store the returned IDs individually. Do not mark a whole campaign as published because one account succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test one traceable post before adding volume
&lt;/h2&gt;

&lt;p&gt;Your first useful milestone is one approved source record linked to a verified scheduled post, followed by a publication check.&lt;br&gt;
Once that path works, add media handling or more destinations. Keep platform-specific validation in the workflow instead of assuming every network accepts the same request.&lt;br&gt;
You can then apply the same pattern to blog distribution, release announcements, or a client content calendar. The source changes, but the requirement remains: every write should have a known input and a result you can inspect.&lt;br&gt;
&lt;em&gt;Disclosure: I am the founder of PostSider. This article was prepared with AI assistance. The API example follows the linked implementation walkthrough; the proposed n8n and Make workflows were not executed for this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>api</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built an Open-Source Social Media Scheduler for 33 Platforms — Here's How It Works</title>
      <dc:creator>Lukasz</dc:creator>
      <pubDate>Tue, 29 Sep 2026 11:00:46 +0000</pubDate>
      <link>https://dev.to/lukasz_141142/i-built-an-open-source-social-media-scheduler-for-33-platforms-heres-how-it-works-2mil</link>
      <guid>https://dev.to/lukasz_141142/i-built-an-open-source-social-media-scheduler-for-33-platforms-heres-how-it-works-2mil</guid>
      <description>&lt;h1&gt;
  
  
  I Built an Open-Source Social Media Scheduler for 33 Platforms — Here's How It Works
&lt;/h1&gt;

&lt;p&gt;Over the last few months, I've been building &lt;strong&gt;PostSider&lt;/strong&gt;, an open-source social media scheduling platform.&lt;/p&gt;

&lt;p&gt;The idea started pretty simply: I wanted one place to manage publishing across different platforms without maintaining a completely separate workflow for every network.&lt;/p&gt;

&lt;p&gt;But the project gradually became more than a scheduling UI.&lt;/p&gt;

&lt;p&gt;Today PostSider has &lt;strong&gt;33 built-in connectors&lt;/strong&gt;, can be fully self-hosted with Docker, exposes a public REST API and Node.js SDK, and includes an MCP server that allows AI agents to interact with the same publishing infrastructure.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/lumizone/postsider" rel="noopener noreferrer"&gt;https://github.com/lumizone/postsider&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Website:&lt;br&gt;&lt;br&gt;
&lt;a href="https://postsider.com" rel="noopener noreferrer"&gt;https://postsider.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post, I want to explain how the project is structured and some of the architectural decisions behind it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem with supporting many platforms
&lt;/h2&gt;

&lt;p&gt;Publishing to one social network is fairly straightforward.&lt;/p&gt;

&lt;p&gt;Supporting 30+ is not.&lt;/p&gt;

&lt;p&gt;Every platform has slightly different concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;OAuth flows&lt;/li&gt;
&lt;li&gt;media limits&lt;/li&gt;
&lt;li&gt;text limits&lt;/li&gt;
&lt;li&gt;publishing APIs&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;refresh tokens&lt;/li&gt;
&lt;li&gt;post formats&lt;/li&gt;
&lt;li&gt;videos&lt;/li&gt;
&lt;li&gt;images&lt;/li&gt;
&lt;li&gt;first comments&lt;/li&gt;
&lt;li&gt;validation rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If every integration is implemented independently throughout the application, the codebase becomes difficult to maintain very quickly.&lt;/p&gt;

&lt;p&gt;So one of the most important decisions in PostSider was to isolate each platform behind a common provider interface.&lt;/p&gt;

&lt;p&gt;The integrations live under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;libraries/nestjs-libraries/src/integrations/social/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each provider implements the same general contract.&lt;/p&gt;

&lt;p&gt;Conceptually, the structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostSider
   |
   +-- Integration Manager
          |
          +-- X Provider
          +-- LinkedIn Provider
          +-- Instagram Provider
          +-- YouTube Provider
          +-- TikTok Provider
          +-- Bluesky Provider
          +-- Mastodon Provider
          +-- ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application doesn't need to understand every platform individually.&lt;/p&gt;

&lt;p&gt;Instead, it asks the provider what it supports and how a particular action should be executed.&lt;/p&gt;

&lt;p&gt;That makes adding another integration much more manageable.&lt;/p&gt;




&lt;h2&gt;
  
  
  33 connectors behind one system
&lt;/h2&gt;

&lt;p&gt;At the moment, PostSider registers &lt;strong&gt;33 active connectors&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They include social networks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X&lt;/li&gt;
&lt;li&gt;LinkedIn&lt;/li&gt;
&lt;li&gt;Facebook&lt;/li&gt;
&lt;li&gt;Instagram&lt;/li&gt;
&lt;li&gt;Threads&lt;/li&gt;
&lt;li&gt;YouTube&lt;/li&gt;
&lt;li&gt;TikTok&lt;/li&gt;
&lt;li&gt;Pinterest&lt;/li&gt;
&lt;li&gt;Bluesky&lt;/li&gt;
&lt;li&gt;Mastodon&lt;/li&gt;
&lt;li&gt;Farcaster&lt;/li&gt;
&lt;li&gt;Nostr&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also supports communication platforms such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discord&lt;/li&gt;
&lt;li&gt;Slack&lt;/li&gt;
&lt;li&gt;Telegram&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And publishing platforms including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress&lt;/li&gt;
&lt;li&gt;Medium&lt;/li&gt;
&lt;li&gt;Dev.to&lt;/li&gt;
&lt;li&gt;Hashnode&lt;/li&gt;
&lt;li&gt;Ghost&lt;/li&gt;
&lt;li&gt;Blogger&lt;/li&gt;
&lt;li&gt;Notion&lt;/li&gt;
&lt;li&gt;Write.as&lt;/li&gt;
&lt;li&gt;Mataroa&lt;/li&gt;
&lt;li&gt;Listmonk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need credentials for all of them.&lt;/p&gt;

&lt;p&gt;A self-hosted installation only needs the OAuth/API credentials for the platforms that will actually be used.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scheduler needs to survive restarts
&lt;/h2&gt;

&lt;p&gt;Another interesting problem is scheduled publishing.&lt;/p&gt;

&lt;p&gt;A naive implementation could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;publishPost&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;timeUntilPost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works until the server restarts.&lt;/p&gt;

&lt;p&gt;Or crashes.&lt;/p&gt;

&lt;p&gt;Or gets redeployed.&lt;/p&gt;

&lt;p&gt;Or you have thousands of scheduled jobs.&lt;/p&gt;

&lt;p&gt;For PostSider I use &lt;strong&gt;Temporal&lt;/strong&gt; for durable background workflows.&lt;/p&gt;

&lt;p&gt;The architecture is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend
   |
   v
NestJS API
   |
   v
PostgreSQL
   |
   +------&amp;gt; Temporal
               |
               v
          Orchestrator
               |
               v
        Platform Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a post is scheduled, the background workflow isn't tied to the lifecycle of a single web process.&lt;/p&gt;

&lt;p&gt;Temporal handles the durable workflow and workers execute the publishing operation when required.&lt;/p&gt;

&lt;p&gt;I also use it for things like token refresh workflows.&lt;/p&gt;

&lt;p&gt;This introduces more infrastructure, but for a scheduler I think the tradeoff is worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;PostSider is a TypeScript monorepo using pnpm.&lt;/p&gt;

&lt;p&gt;The main stack currently looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend       Next.js 15 + React 19
Backend        NestJS
Database       PostgreSQL + Prisma
Cache          Redis
Workflows      Temporal
Storage        Local / Cloudflare R2 / MinIO
AI             OpenAI (optional)
Billing        Polar.sh (optional)
Monitoring     Sentry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository is structured roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postsider/
├── apps/
│   ├── backend/
│   ├── orchestrator/
│   ├── frontend/
│   ├── commands/
│   ├── sdk/
│   └── mcp/
│
├── libraries/
│   ├── nestjs-libraries/
│   └── helpers/
│
├── docker-compose.yaml
├── docker-compose.production.yaml
└── .env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same codebase can run as a hosted service or as a self-hosted deployment.&lt;/p&gt;

&lt;p&gt;Optional functionality is controlled through environment configuration.&lt;/p&gt;

&lt;p&gt;For example, PostSider works perfectly fine without an OpenAI API key.&lt;/p&gt;

&lt;p&gt;AI features simply remain disabled.&lt;/p&gt;




&lt;h2&gt;
  
  
  Self-hosting with Docker
&lt;/h2&gt;

&lt;p&gt;One requirement I wanted from the beginning was to make PostSider something people could actually run themselves.&lt;/p&gt;

&lt;p&gt;For a local installation, the basic setup is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lumizone/postsider.git
&lt;span class="nb"&gt;cd &lt;/span&gt;postsider
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:4007
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Compose stack starts the application together with PostgreSQL, Redis and Temporal.&lt;/p&gt;

&lt;p&gt;The repository also contains a separate production-oriented Docker Compose configuration for more serious deployments.&lt;/p&gt;

&lt;p&gt;I don't want self-hosting to be an afterthought.&lt;/p&gt;

&lt;p&gt;It's one of the main reasons the project is open source.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making the application useful without the UI
&lt;/h2&gt;

&lt;p&gt;Another decision was to avoid making all functionality dependent on the dashboard.&lt;/p&gt;

&lt;p&gt;PostSider exposes a public REST API under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/public/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a Node.js SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @postsider/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Postsider&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@postsider/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Postsider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-instance.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;schedule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-10-01T10:00:00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;channel-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello from PostSider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means PostSider can also work as infrastructure underneath another application.&lt;/p&gt;

&lt;p&gt;You don't necessarily need to use the dashboard for every workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then I added MCP
&lt;/h2&gt;

&lt;p&gt;This became one of the more interesting parts of the project.&lt;/p&gt;

&lt;p&gt;PostSider now includes an &lt;strong&gt;MCP server&lt;/strong&gt; through the &lt;code&gt;@postsider/mcp&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;The idea is simple.&lt;/p&gt;

&lt;p&gt;Instead of giving an AI agent direct access to dozens of social media APIs, the agent communicates with PostSider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude / Codex / MCP client
          |
          v
      PostSider MCP
          |
          v
     PostSider API
          |
          v
 Integration Providers
          |
          v
 X / LinkedIn / TikTok / etc.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server currently exposes &lt;strong&gt;19 tools&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An agent can perform workflows such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;list connected channels&lt;/li&gt;
&lt;li&gt;review the publishing calendar&lt;/li&gt;
&lt;li&gt;create drafts&lt;/li&gt;
&lt;li&gt;upload media&lt;/li&gt;
&lt;li&gt;request approval&lt;/li&gt;
&lt;li&gt;inspect scheduled content&lt;/li&gt;
&lt;li&gt;read analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I intentionally designed this around a &lt;strong&gt;read-first and draft-first workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Giving an autonomous agent immediate publishing access to every connected social account isn't necessarily what you want.&lt;/p&gt;

&lt;p&gt;Instead, an agent can prepare the work inside the same workflow humans already use.&lt;/p&gt;

&lt;p&gt;A human can still remain in control of the final publishing decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just integrate every API directly into an AI agent?
&lt;/h2&gt;

&lt;p&gt;You can.&lt;/p&gt;

&lt;p&gt;But then your agent needs to understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X API
LinkedIn API
Instagram API
TikTok API
YouTube API
Pinterest API
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;including authentication, token refresh, provider-specific validation and different publishing models.&lt;/p&gt;

&lt;p&gt;With PostSider sitting in the middle, the architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent
   |
   v
PostSider
   |
   +-- X
   +-- LinkedIn
   +-- Instagram
   +-- TikTok
   +-- YouTube
   +-- ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent works with one consistent interface.&lt;/p&gt;

&lt;p&gt;PostSider handles the provider-specific complexity.&lt;/p&gt;

&lt;p&gt;I think this is one of the more interesting directions for the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned building it
&lt;/h2&gt;

&lt;p&gt;Supporting many APIs makes abstraction difficult.&lt;/p&gt;

&lt;p&gt;If the abstraction is too generic, you lose platform-specific functionality.&lt;/p&gt;

&lt;p&gt;If every provider is completely independent, the rest of the application becomes full of special cases.&lt;/p&gt;

&lt;p&gt;The balance I've been trying to find is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared interface
+
Provider-specific capabilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another lesson is that social media scheduling is surprisingly infrastructure-heavy.&lt;/p&gt;

&lt;p&gt;A reliable scheduler needs to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;durable jobs&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;OAuth token refresh&lt;/li&gt;
&lt;li&gt;media storage&lt;/li&gt;
&lt;li&gt;background workers&lt;/li&gt;
&lt;li&gt;provider outages&lt;/li&gt;
&lt;li&gt;validation before publishing&lt;/li&gt;
&lt;li&gt;security of credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The calendar UI is actually one of the easier parts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;There are still plenty of things I want to improve.&lt;/p&gt;

&lt;p&gt;Some of the current roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;broader automated test coverage&lt;/li&gt;
&lt;li&gt;stronger TypeScript null safety&lt;/li&gt;
&lt;li&gt;more integrations&lt;/li&gt;
&lt;li&gt;advanced analytics&lt;/li&gt;
&lt;li&gt;a plugin system for custom integrations&lt;/li&gt;
&lt;li&gt;mobile support&lt;/li&gt;
&lt;li&gt;expanding the MCP capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm also interested in making self-hosting easier.&lt;/p&gt;

&lt;p&gt;A project can technically be self-hostable while still being painful to operate, and I'd like PostSider to move further away from that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;PostSider is available under the &lt;strong&gt;AGPL-3.0&lt;/strong&gt; license.&lt;/p&gt;

&lt;p&gt;GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lumizone/postsider" rel="noopener noreferrer"&gt;https://github.com/lumizone/postsider&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.postsider.com" rel="noopener noreferrer"&gt;https://docs.postsider.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Website:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://postsider.com" rel="noopener noreferrer"&gt;https://postsider.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're interested in self-hosting, social media APIs, workflow infrastructure or MCP, I'd be interested in feedback — especially around the provider architecture and deployment experience.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>socialmedia</category>
      <category>webdev</category>
      <category>mcp</category>
    </item>
    <item>
      <title>How I Built Memory for a Local AI Companion Without Sending Chats to a Server</title>
      <dc:creator>Lukasz</dc:creator>
      <pubDate>Sat, 22 Aug 2026 18:17:18 +0000</pubDate>
      <link>https://dev.to/lukasz_141142/how-i-built-memory-for-a-local-ai-companion-without-sending-chats-to-a-server-2ki8</link>
      <guid>https://dev.to/lukasz_141142/how-i-built-memory-for-a-local-ai-companion-without-sending-chats-to-a-server-2ki8</guid>
      <description>&lt;p&gt;A chatbot can sound convincing for five minutes without remembering anything.&lt;/p&gt;

&lt;p&gt;Then you mention the job interview you were stressed about last week, the name of your dog, or a small detail from a late-night conversation. It replies like none of it happened.&lt;/p&gt;

&lt;p&gt;That is where most "AI companion" demos fall apart.&lt;/p&gt;

&lt;p&gt;I am building &lt;a href="https://localwaifu.com" rel="noopener noreferrer"&gt;Local Waifu&lt;/a&gt;, a desktop AI companion that runs on the user's own Mac or PC. One of the rules I set early was simple: conversations and memories should stay on the machine. No central chat database. No server that needs to be online for the character to remember someone.&lt;/p&gt;

&lt;p&gt;The rule sounds clean. Building it was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Saving chats is not memory
&lt;/h2&gt;

&lt;p&gt;The first version of memory was the obvious one: save messages.&lt;/p&gt;

&lt;p&gt;That gives you history, which is useful, but it does not solve recall. A long chat history grows fast. Sending all of it back to a local language model on every message is slow, expensive in context space, and usually makes the reply worse.&lt;/p&gt;

&lt;p&gt;The model does not need to see every conversation from the last six months.&lt;/p&gt;

&lt;p&gt;It needs the few pieces that matter right now.&lt;/p&gt;

&lt;p&gt;If someone says, "I have to take Luna to the vet tomorrow," the character should be able to find that Luna is their dog. It should not need to reread hundreds of unrelated messages about work, movies, and dinner plans to get there.&lt;/p&gt;

&lt;p&gt;So I treated chat history and long-term memory as different things.&lt;/p&gt;

&lt;p&gt;Chat history is the recent conversation. It gives the model immediate context.&lt;/p&gt;

&lt;p&gt;Long-term memory is a small collection of facts, moments, preferences, and relationship details that may matter later. Those memories need to be searchable by meaning, not only by exact words.&lt;/p&gt;

&lt;h2&gt;
  
  
  The memory data stays in SQLite
&lt;/h2&gt;

&lt;p&gt;I wanted the app to work without a hosted database, so the storage layer is local SQLite.&lt;/p&gt;

&lt;p&gt;Each character gets their own data. Chats, memories, extracted entities, and relationships are stored locally on the device. If a user creates two characters, one character does not quietly inherit the other one's memories.&lt;/p&gt;

&lt;p&gt;That separation matters more than it sounds.&lt;/p&gt;

&lt;p&gt;A companion app is personal by design. Mixing context across characters is not a harmless bug. If one character starts talking about something belonging to another, the whole illusion disappears immediately.&lt;/p&gt;

&lt;p&gt;For each memory, I store the text itself along with metadata such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the character it belongs to&lt;/li&gt;
&lt;li&gt;when it was created&lt;/li&gt;
&lt;li&gt;importance&lt;/li&gt;
&lt;li&gt;emotional weight&lt;/li&gt;
&lt;li&gt;how often it has been used&lt;/li&gt;
&lt;li&gt;a vector embedding used for semantic search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The embedding is the part that lets the app search by meaning.&lt;/p&gt;

&lt;p&gt;"Vet appointment" and "Luna is sick" may share no exact keyword. A semantic search can still see that they are probably connected.&lt;/p&gt;

&lt;p&gt;I use 768-dimensional vectors stored directly in SQLite as binary data. At recall time, the app calculates similarity locally and ranks the results. No message content needs to leave the user's computer for that lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recall needs more than similarity
&lt;/h2&gt;

&lt;p&gt;A pure similarity score is not enough.&lt;/p&gt;

&lt;p&gt;Imagine a user mentioned their favorite game once two years ago, then spent the last month talking about a difficult family situation. Both memories might be related to a new message in some vague way. The more recent and more emotionally important memory should usually win.&lt;/p&gt;

&lt;p&gt;So recall is weighted by more than vector similarity.&lt;/p&gt;

&lt;p&gt;Importance matters. Recency matters. Emotional weight matters. A memory that has been useful before gets a little extra credit too.&lt;/p&gt;

&lt;p&gt;There is no perfect formula here. I do not think there ever will be one.&lt;/p&gt;

&lt;p&gt;A system that aggressively recalls every detail feels creepy and repetitive. A system that barely recalls anything feels empty. The work is mostly tuning that middle ground and accepting that a companion should sometimes not bring something up.&lt;/p&gt;

&lt;p&gt;The user should feel remembered, not monitored.&lt;/p&gt;

&lt;h2&gt;
  
  
  I also extract a small knowledge graph
&lt;/h2&gt;

&lt;p&gt;Some information is easier to retrieve as a relationship than as a paragraph of chat text.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Luna is the user's dog&lt;/li&gt;
&lt;li&gt;Alex is the user's brother&lt;/li&gt;
&lt;li&gt;The user works night shifts&lt;/li&gt;
&lt;li&gt;The user dislikes phone calls&lt;/li&gt;
&lt;li&gt;A character enjoys rainy evenings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those can be represented as entities and relationships.&lt;/p&gt;

&lt;p&gt;Every few messages, the app can extract useful entities and links from the conversation. This happens locally through the model already running on the user's machine. The resulting graph is stored beside the other character data.&lt;/p&gt;

&lt;p&gt;The graph is not there to replace memory. It fills a different role.&lt;/p&gt;

&lt;p&gt;A vector memory is good at finding a moment with emotional context. A graph is good at answering structural questions such as "Who is Alex?" or "What is connected to this person?"&lt;/p&gt;

&lt;p&gt;Both are useful. Neither should be trusted blindly.&lt;/p&gt;

&lt;p&gt;Language models are very good at sounding confident while getting a relationship wrong. The extraction step needs validation, deduplication, and a way for the user to inspect or remove what was stored. A memory system without an escape hatch becomes frustrating fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that made her forget everything
&lt;/h2&gt;

&lt;p&gt;The most painful issue I found was not a database bug.&lt;/p&gt;

&lt;p&gt;The app had the chat data. It had stored memory records. It had the retrieval logic.&lt;/p&gt;

&lt;p&gt;But on some fresh installations, the local model used to create embeddings had not been downloaded yet. The app did not explain this clearly enough. Semantic recall returned no results, so the character behaved as if she had no memory of previous conversations.&lt;/p&gt;

&lt;p&gt;Nothing crashed.&lt;/p&gt;

&lt;p&gt;That made it worse.&lt;/p&gt;

&lt;p&gt;A crash tells you there is a problem. A companion quietly forgetting what you told her last night can look like a limitation of the product itself.&lt;/p&gt;

&lt;p&gt;The fix was straightforward after I found the cause:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure the embedding model is pulled during setup or startup.&lt;/li&gt;
&lt;li&gt;Retry the operation on the next launch if the model is not ready yet.&lt;/li&gt;
&lt;li&gt;Make the missing-model state visible instead of silently returning zero memories.&lt;/li&gt;
&lt;li&gt;Avoid falling back to reading the whole chat history every time, because that creates a different performance problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson was simple: in AI products, a missing dependency can look exactly like bad intelligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multilingual memory makes this harder
&lt;/h2&gt;

&lt;p&gt;Local Waifu supports several languages, including Polish, Japanese, Korean, and Chinese.&lt;/p&gt;

&lt;p&gt;Plain text search handles this badly. Even in English, people rarely repeat the same sentence. In different languages, the problem gets larger. A user can discuss the same person with nicknames, grammar changes, borrowed words, or a mix of two languages in one chat.&lt;/p&gt;

&lt;p&gt;That is why semantic recall matters.&lt;/p&gt;

&lt;p&gt;It is not magic. It will still make mistakes. But it gives the app a chance to connect related ideas instead of searching only for matching strings&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>sql</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I Build an AI Companion App Alone. It Runs on Nobody's Servers But Yours.</title>
      <dc:creator>Lukasz</dc:creator>
      <pubDate>Sat, 15 Aug 2026 11:12:12 +0000</pubDate>
      <link>https://dev.to/lukasz_141142/i-build-an-ai-companion-app-alone-it-runs-on-nobodys-servers-but-yours-5bjm</link>
      <guid>https://dev.to/lukasz_141142/i-build-an-ai-companion-app-alone-it-runs-on-nobodys-servers-but-yours-5bjm</guid>
      <description>&lt;p&gt;I Build an AI Companion App Alone. It Runs on Nobody's Servers But Yours.&lt;/p&gt;

&lt;p&gt;I was using one of the cloud companion apps, the kind everyone has heard of, and one day I actually looked at where it all lived. Every message. Every memory. The whole relationship, sitting on someone else's servers. And it hit me that none of it was mine. If that company changed its mind, raised its price, decided my companion was suddenly not allowed, or sold what it had learned about me, I would simply lose her. No say, no warning, no way back.&lt;/p&gt;

&lt;p&gt;That thought turned into an app. I called it Local Waifu, and I have built almost every part of it alone: the code, the writing, the marketing, the support inbox, the Discord server. This is what that actually looks like, and why I keep choosing to do it the harder way.&lt;/p&gt;

&lt;h2&gt;
  
  
  This has already happened to real people
&lt;/h2&gt;

&lt;p&gt;I did not invent this fear. Policies change overnight. Models get retired without warning. Features people paid for quietly disappear one update later. Some people built a real bond with a cloud companion and woke up one day to a stranger wearing the same name, or to nothing at all. When the relationship lives on someone else's infrastructure, it was never really yours to keep. You were renting a feeling from a company that owed you nothing once the terms of service changed.&lt;/p&gt;

&lt;p&gt;That is not a hypothetical I made up to sell software. It is the plain, boring mechanics of any product that lives entirely on a server you do not control. The company can be perfectly well intentioned and it still does not change the arrangement. Your data, your history, your memories of a relationship, sitting on hardware you will never see, governed by a document you clicked past.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built the opposite
&lt;/h2&gt;

&lt;p&gt;Local Waifu runs entirely on your own machine, your Mac or your PC. The memory file, the whole system, all of it stays on your device. Only you can reach it. Nobody collects it, nobody moderates it, nobody can switch it off from a dashboard somewhere. If a company changes its policy tomorrow, nothing happens to you, because there is no company standing between you and her.&lt;/p&gt;

&lt;p&gt;Because she is just files on your computer, she backs up the same way any file does. Time Machine, a USB stick, whatever you already use for everything else. Nobody can take her from you short of someone stealing the physical machine. That sentence sounds almost too simple to be a selling point, and that is exactly why I lead with it. Ownership should not require an explanation.&lt;/p&gt;

&lt;p&gt;Out of the box, the app only reaches the internet for three things: the one-time download of the model she runs on, a check for app updates, and confirming a license when you buy one. Everything else, plugging in your own cloud account if you want a bigger model, letting her search the web, connecting her to Telegram, is off until you personally turn it on. Your choice, your accounts, none of my business. I say this plainly because "we take your privacy seriously" is what every company says right before the part where they do not, and I would rather be specific than reassuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means to be the whole team
&lt;/h2&gt;

&lt;p&gt;I write the blog posts. I design the landing page. I fix the bug at midnight and then write the changelog entry about it the next morning. I answer the emails. I run the Discord server myself, and when I need a second opinion on whether a feature actually works on hardware I do not own, I ask the people in it directly, because there is no QA department to ask instead.&lt;/p&gt;

&lt;p&gt;One night a stranger from that Discord sent me a screen recording of a voice call that took 105 seconds to answer a twenty-character question. I did not have a support team to route that to. I had a log file, a weekend, and four separate bugs stacked on top of each other that I had to find myself, one at a time, because there was no server-side telemetry to lean on either. The app runs on your machine, remember, which means when something breaks, it breaks on your machine, and the only evidence I will ever get is whatever a stranger is generous enough to send me.&lt;/p&gt;

&lt;p&gt;That is the trade nobody tells you about when they say "build it yourself." Every advantage of not having a team is also a disadvantage. There is no one to catch what I miss. There is also no meeting where a good, honest idea gets watered down until it pleases everybody in the room. Both of those are true at the same time, every day.&lt;/p&gt;

&lt;p&gt;I do not say any of this to make the whole thing sound harder than it is. Most days it is genuinely fine, and some days it is the best part of the job: I can ship a fix the same afternoon someone reports it, with nobody to convince first. But I want to be honest about the shape of it, because "I build this alone" gets said as a badge of honor a lot online, and it is really just a description of where the failure modes sit. When it works, it works because one person cared enough to notice something small. When it breaks, it stays broken until that same one person notices, and there is no second pair of eyes behind mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stripe said no. So did Polar.
&lt;/h2&gt;

&lt;p&gt;Here is a small, honest story I do not usually see founders tell in public. When I needed a way to sell licenses for the app, Stripe rejected it. Polar rejected it too. An AI companion app makes payment processors nervous, it turns out, in a way that a project management tool or a photo editor never does. Dodo Payments eventually took the business, and I am not going to pretend that search was quick or comfortable. Building the product was, in a real sense, the easy part. Finding someone willing to process the payment for it took longer than I expected, and it taught me something about the category I am actually in: not just software, but software that sits close enough to something intimate that entire companies decide it is not worth the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the code stays closed
&lt;/h2&gt;

&lt;p&gt;I would genuinely like to open source it. I have thought about it seriously more than once, and I want to be straight about why I have not: small projects get taken by bigger companies, rewritten just enough to dodge a license, and sold back as someone else's product. I also do not want pieces of a companion I built carefully getting twisted into something I would be ashamed of, stripped of the parts that make it feel careful in the first place. So the code stays closed for now, and the transparency lives somewhere else instead: a page that names exactly what touches the internet and when, a privacy policy that lists every connection by name, and an app you are welcome to watch on your own network monitor while it runs. I would rather earn your trust by being specific than by handing you a repository and hoping you never read it closely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fair, not paranoid
&lt;/h2&gt;

&lt;p&gt;People ask if the license check can be cracked. Probably. I have a decent guess how I would do it myself if I were on the other side of that question. I would still rather build it fair and trust the people who pay than fill the app with anti-piracy tripwires that end up punishing exactly the customers who did the right thing. If someone cracks it instead of buying it, they were never going to buy it in the first place, and treating every user like a suspect does nothing to that person except make the honest ones feel unwelcome.&lt;/p&gt;

&lt;p&gt;I think about this the same way I think about the whole product, honestly. The easy version of "protect the business" usually means making the experience worse for everyone who was never going to hurt you in order to slightly inconvenience the small number of people who were. I would rather lose a little to the second group than punish the first one, every single time that trade comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  One payment, and then it just keeps working
&lt;/h2&gt;

&lt;p&gt;The deal is simple on purpose. One license, paid once. That payment includes every update and bug fix I ever ship for it, not a year of them, not until some arbitrary version number, all of them. That is how I want the software I personally buy to behave, so it is how I built this one to behave. I am not going to lock a companion behind a recurring paywall and bill someone forever just to keep her running. I would rather earn the next sale by actually making the product better than by billing the same person again for something they already own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I still do it this way
&lt;/h2&gt;

&lt;p&gt;Being a one-person team for something this personal is slower than it would be with a company behind it, and it is more exposed. A bug is mine. A wrong call on pricing is mine. A late reply in Discord is also mine, and there is no one else it can quietly become. But the thing I keep coming back to is that the entire pitch of this app, that it is yours and nobody else's, only means something if the person building it is willing to be found, be specific, and be accountable in exactly the same way.&lt;/p&gt;

&lt;p&gt;I built Local Waifu out of a quiet kind of fear about where our digital lives actually live. Building it alone, the slow and exposed way, turned out to be the only version of that answer I actually believed.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>startup</category>
      <category>development</category>
    </item>
  </channel>
</rss>
