Introduction

postctl

Terminal social media and blogging manager. Write posts in Markdown, schedule them, and publish to Twitter/X, LinkedIn, Threads, Mastodon, Bluesky, Facebook, Telegram, Discord, Reddit, Dev.to, Hashnode, and Medium from the command line or a full TUI.

Supported platforms: Twitter/X · LinkedIn · Threads · Mastodon · Bluesky · Facebook · Telegram · Discord · Reddit · Dev.to · Hashnode · Medium


Quick Start

  1. Install

    git clone https://github.com/aeon022/postctl && cd postctl
    ./setup.sh
  2. Authenticate with a platform

    postctl auth --platform twitter
  3. Write a post — create a Markdown file (see Post Format):

    ---
    platform: twitter
    title: My first post
    ---
    
    Hello from postctl.
  4. Import the file

    postctl import my-post.md
  5. Publish immediately or schedule

    postctl post <ID>
    postctl schedule <ID> --time 2026-07-10T09:00:00+02:00
  6. Open the TUI to manage everything

    postctl tui

Cheatsheet

postctl                                  Open TUI (default)
postctl tui                              Open TUI explicitly

postctl auth --platform PLATFORM         Authenticate with a platform
postctl config [--show] [--set K V]      View or set config values
postctl config test                      Test connection to configured platform APIs
postctl rss add URL                      Add an RSS feed URL
postctl rss list                         List all configured RSS feeds
postctl rss remove URL                   Remove a configured RSS feed
postctl rss import                       Fetch feeds and import articles as drafts

postctl import FILE_OR_DIR               Import Markdown post(s)
postctl list [--platform P] [--status S] [--campaign C] [--format human|json]
postctl template --platform PLATFORM     Generate a post template

postctl post ID [--dry-run]              Publish a post immediately (alias: publish)
postctl publish ID [--dry-run]           Publish a post immediately
postctl schedule ID [--time DATETIME] [--queue] Schedule a post (RFC3339) or to the queue
postctl cancel ID                        Cancel a scheduled post
postctl delete ID                        Delete a post locally and remotely
postctl campaign list                    List all campaigns
postctl campaign post NAME [--dry-run]   Publish all posts in a campaign

postctl generate URL                     AI-generate a post from a URL
postctl repurpose ID --platform TARGET [--tone TONE] Repurpose a post with custom tone

postctl git-hook install [--dir DIR]     Install a post-commit git hook
postctl git-hook uninstall               Uninstall the git hook

postctl analytics [--platform P] [--format human|json]
postctl daemon [--dry-run]               Run the background scheduler
postctl mcp                              Start the MCP server (stdio)
postctl version                          Print version

Post Markdown Format

Frontmatter Fields

Field Required Values / Format Description
platform Yes twitter, linkedin, threads, mastodon, bluesky Target platform
title No String Internal label (not published)
campaign No String slug Groups posts into a campaign
schedule No RFC3339 or "queue" Scheduled publish time or Smart Queue

Body Format

Write the post body in plain Markdown below the closing --- of the frontmatter block.

  • LinkedIn, Threads, Mastodon, Bluesky: Single body. No separators.
  • Twitter/X threads: Separate individual tweets with a line containing only ---. Each segment becomes one tweet in the thread.

Twitter Thread Example

---
platform: twitter
title: Launch announcement
campaign: product-launch
schedule: 2026-07-10T09:00:00+02:00
---

This is the first tweet. Max 280 characters for Twitter/X.

---

Second tweet in the thread.

---

Third tweet. Threads are Twitter-only.

CLI Reference

Authentication and Configuration

Command Description
postctl auth --platform PLATFORM Authenticate with the given platform (OAuth flow)
postctl config --show Print the current configuration
postctl config --set KEY VALUE Set a configuration value
postctl config test Run connectivity diagnostics for all configured platform APIs

RSS Feed Importer

Command Description
postctl rss add URL Add a new RSS feed URL to configuration
postctl rss list List all configured RSS feeds
postctl rss remove URL Remove a configured RSS feed
postctl rss import Fetch RSS feeds and import new articles as drafts

Content Management

Command Description
postctl import FILE_OR_DIR Import one Markdown file or a directory of files
postctl list List posts; filter with --platform, --status, --campaign; format with --format human|json
postctl template --platform PLATFORM Print a Markdown template for the given platform
postctl generate URL AI-generate a draft post from the article at URL
postctl repurpose ID --platform TARGET [--tone TONE] Repurpose an existing post with an optional custom tone

Publishing

Command Description
postctl post ID Publish post immediately (alias: publish)
postctl publish ID Publish post immediately
postctl post ID --dry-run Simulate publishing without sending
postctl schedule ID --time DATETIME Set or update the scheduled publish time
postctl schedule ID --queue Schedule a post to the next available queue slot
postctl cancel ID Cancel a scheduled post (resets status to draft)
postctl delete ID Delete a post from the local database (and remote platform if published)
postctl campaign list List all campaigns with post counts
postctl campaign post NAME Publish all posts in a campaign
postctl campaign post NAME --dry-run Dry-run campaign publish
postctl git-hook install [--dir DIR] Install local git post-commit hook for auto-import
postctl git-hook uninstall Remove local git post-commit hook
postctl daemon Start the background scheduler daemon
postctl daemon --dry-run Run daemon in dry-run mode

Analytics

Command Description
postctl analytics Show analytics across all platforms
postctl analytics --platform PLATFORM Filter to one platform
postctl analytics --format json Output as JSON

MCP Server

Command Description
postctl mcp Start the MCP server on stdio for use by AI agents

TUI Guide

Launch with postctl or postctl tui.

Views

View Description
Posts list Main view; shows all posts with status badges (draft / scheduled / posted / failed)
Detail Full post content and metadata
Editor Write or edit post body and frontmatter fields
Schedule Set or adjust the scheduled time for a post
Analytics Platform-level metrics overview
History Log of past publish events
Settings App configuration
Readme In-app documentation

Switch between views using tabs or the keybindings below.

Keybindings

Posts List

Key Action
j / k Navigate up and down
Space Toggle post selection for mass/bulk actions
Enter Open detail view
n New post
e Edit selected post
d Delete selected post(s) locally and remotely (if published)
p Publish selected post(s) immediately
s Schedule selected (or highlighted) post(s) to queue slots
Esc Clear bulk selections (or clear campaign filter)
Tab Switch tabs
q Quit

Detail View

Key Action
Esc Back to list
e Edit post
p Publish post immediately
d Delete post locally and remotely
r Repurpose post

Editor

Key Action
Ctrl+S Save
Esc Cancel
Tab Move between fields

MCP — AI Integration

postctl ships a built-in MCP server that exposes all core operations to AI agents. This lets tools like Claude Desktop create, schedule, and publish posts on your behalf.

[!IMPORTANT] Publishing and scheduling through MCP are off by default — an AI agent that reads untrusted text (mail, web pages) could be tricked into posting. Without opt-in, create_post saves drafts and publish_post / schedule_post refuse. To allow them, start the MCP server with POSTCTL_MCP_ALLOW_PUBLISH=1 in its environment (dry_run always works).

Claude Desktop Configuration

Add the following to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "postctl": {
      "command": "postctl",
      "args": ["mcp"]
    }
  }
}

Restart Claude Desktop after saving. The postctl binary must be on your PATH.

MCP Tools

Tool Parameters Description
list_posts platform, status, campaign (all optional) List posts with optional filters
get_post id Retrieve full post content and metadata by ID
create_post platform, body, title, campaign, schedule Create a draft or scheduled post
publish_post id, dry_run Publish a post immediately
schedule_post id, schedule (RFC3339) Set or update the scheduled publish time
list_campaigns — List all campaigns with total count and per-status breakdown
get_campaign name, status (optional filter) Get all posts in a campaign with full content

For Twitter threads, separate tweets with \n---\n in the body field when calling create_post.

Schedule values must be RFC3339, e.g. 2026-07-10T09:00:00+02:00.

AI Workflow Examples

Plan a campaign from an article

“Read the article at https://example.com/blog/launch, then create a five-post campaign called launch-week with one post per day starting Monday. Use twitter for three posts and linkedin for two.”

Claude calls create_post for each post with appropriate bodies, the campaign name, and staggered schedule values derived from the article content.

Review and publish scheduled posts

“Show me everything scheduled for this week and publish any posts that look ready.”

Claude calls list_posts with status: scheduled, presents the results for your review, then calls publish_post for each approved post — or all of them at once if you confirm.

Repurpose a blog post across platforms

“Take post abc123 and create adapted versions for LinkedIn and Threads.”

Claude calls get_post to retrieve the original, then calls create_post twice — once for linkedin and once for threads — adapting tone and length for each platform automatically.


Security

  • Local files are owner-only: config.yaml (API secrets), the database and the scheduler log are written with 0600 permissions; older, world-readable files are tightened automatically on the next start.
  • Token encryption: stored tokens use AES-256-GCM with a random per-installation key in ~/.config/postctl/encryption.key (0600, never inside a synced data directory). Installations that predate the key file switch with postctl security migrate; postctl security status shows what you use. Copy the key file to your other devices — a synced database is unreadable without it.
  • Backups (postctl config export / config import) are encrypted with AES-256-GCM, key derived with PBKDF2-HMAC-SHA256 (600k iterations, random salt). Older backups still restore.
  • Uploads are checked: only real PNG/JPEG/GIF/WebP files (any extension, symlinks resolved, ≤ 25 MB) are uploaded from local paths — a post can never publish e.g. an SSH key.
  • No secrets in logs: token-like values are stripped from logs, history and error messages. postctl config set KEY - reads a secret from stdin instead of the command line.
  • OAuth: random state, PKCE, callback only on 127.0.0.1; forged callbacks cannot abort a login.
  • X cookie mode is unofficial, off by default and needs twitter.unofficial_cookie_mode: true.

Report vulnerabilities privately: see SECURITY.md.

Platform Notes

Platform Character Limit Threads Images
Twitter/X 280 per tweet Yes, separate with --- Supported
LinkedIn ~3,000 recommended No Supported
Threads 500 No At least one recommended (Meta requirement)
Mastodon 500 (instance default) No Supported
Bluesky 300 No Supported
Facebook ~63,206 No Supported
Telegram 4,096 (1,024 for captions) No Supported
Discord 2,000 No Supported
Reddit 40,000 No Not Supported
Dev.to ~100,000 No Not Supported
Hashnode ~100,000 No Not Supported
Medium ~100,000 No Not Supported

Twitter threads have no hard post count limit, but keep threads focused. Other platforms do not support thread-style multi-part posts — use a single body for those platforms.

[!WARNING] API Rate Limits & Bulk Publishing: Publishing multiple posts simultaneously or in quick succession can lead to API rate limits or permanent account bans (especially on federated networks like Mastodon). Always space out posts over time (e.g., at least 15-30 minutes delay between consecutive publishing events).

Built-in safeguards in the scheduler/daemon: several overdue posts on one platform are rescheduled in 20-minute steps (log tag [SAFETY]), and on Twitter/X at least 60 minutes must pass between two published posts — a post that comes due earlier simply waits and goes out on a later check.


Recent changes (October 2026)

  • Scheduled posts. On Dev.to and Medium a post whose scheduled time has arrived is now published; a draft is only created when the scheduled time is still in the future (before, due posts were saved as drafts while postctl showed them as posted).

  • Discord. Posts now return the real message ID (the webhook is called with ?wait=true), so they can be deleted later.

  • postctl delete. It removes the post on the platform first and only then the local record. Twitter deletion is not implemented, and old Discord posts stored with the placeholder ID webhook-posted cannot be deleted remotely — in both cases it now stops with an error and keeps the local record instead of pretending success.

  • OAuth callback. The local callback page HTML-escapes the error text it shows.

  • Tests. The platform clients are covered by tests against mock HTTP servers (no network, no tokens needed).


Architecture

Markdown files
      |
   postctl import
      |
      v
SQLite  (~/.local/share/postctl/postctl.db)
      |
      +---> TUI (Bubbletea)    ---> Platform APIs  (Twitter, LinkedIn, Threads, Mastodon, Bluesky)
      |
      +---> MCP server (stdio) ---> AI agents  (Claude Desktop, etc.)
      |
      +---> postctl daemon     ---> scheduled publish via platform APIs

Requirements: macOS or Linux · Go 1.21+ · API credentials for each platform you use


Profiles — separate accounts for Work / Private / per-project

By default postctl uses a single config and database. To keep entirely separate sets of credentials, posts, and scheduling — e.g. a private and a work Twitter account, or one set per client project — pass --profile <name> (or set POSTCTL_PROFILE) on any command:

postctl --profile work config set twitter.client_id "..."
postctl --profile work config set twitter.client_secret "..."
postctl --profile work tui

postctl --profile privat config set twitter.client_id "..."
postctl --profile privat tui

A profile is created automatically the first time you use its name — there’s no separate “create” step. Each profile gets its own config file (~/.config/postctl/profiles/<name>/config.yaml) and its own database (~/.local/share/postctl/profiles/<name>/postctl.db), completely independent of the default profile and of each other — no shared credentials file, no mixed post history.

postctl profile list      # see every profile that's been used, and which is active
postctl profile           # show just the currently active profile

Running with no --profile always uses the original default (~/.config/postctl/config.yaml) — existing setups are unaffected. Each profile can also be synced across devices independently via its own data_dir (see below) — e.g. sync “work” to a work Dropbox and “privat” to your personal iCloud Drive.


Sharing your data directory across devices

By default postctl’s database lives at ~/.local/share/postctl/postctl.db, local to this machine. To share it across devices, set data_dir (in ~/.config/postctl/config.yaml) to a folder you already sync yourself — iCloud Drive, Dropbox, Syncthing, etc:

data_dir: "~/Library/Mobile Documents/com~apple~CloudDocs/postctl"

Once set, postctl automatically switches its SQLite journal mode from WAL to rollback-journal — WAL splits the database across multiple files that a folder-sync client can’t update atomically together, so this switch keeps the directory down to a single consistent file whenever postctl isn’t actively writing. A same-machine lock also prevents two postctl processes from opening the database at once (run postctl doctor to see the current mode and path). This only protects against the same-machine and stale-snapshot failure modes, not two machines editing at the exact same instant; an undownloaded iCloud file is reported explicitly rather than as a bare error.

(This is separate from postctl config export/import, which packages your config and database into one encrypted file for a manual one-time transfer — the setting above is for keeping them continuously in sync instead.)


Pricing

The core of postctl is free and open-source (MIT) — unlimited posts and drafts on up to 2 connected accounts. postctl Pro (subscription: €4.90/month or €39/year, cancel any time) removes the account limit and funds development; subscribe on Polar.sh and activate with:

postctl license activate <key>

🎉 Launch special: use code postctl2026 on the yearly plan for 37% off the first year — through December 31, 2026.

How the subscription works: postctl re-checks the license with Polar about once a day (a previously valid license keeps working offline for up to 21 days). If a subscription ends, nothing is deleted: the free core keeps working with your first two connected networks, and posts for further networks stay scheduled until Pro is active again. Licenses bought as “Lifetime” before 2026-10-18 stay valid.

License

See LICENSE.

Installation & Setup Guide

Get started with postctl by building the binary and initializing your local developer workspace.

System Requirements

  • Operating System: macOS, Linux, or Windows (WSL recommended).
  • Go Toolchain: Go 1.22 or higher installed on your system.
  • SQLite: Local SQLite client library (CGO-free driver is compiled directly inside).

One-Step Automatic Setup

The easiest way to initialize the application, databases, and dependencies is to run the interactive setup script:

chmod +x setup.sh
./setup.sh

The setup script will:

  1. Verify your local Go environment.
  2. Download required packages (including Bubble Tea, Cobra, and SQLite).
  3. Build the production binary postctl.
  4. Create the configuration directory at ~/.config/postctl/.
  5. Generate a default config.yaml and empty SQLite database.

Manual Compilation

If you prefer to compile the application manually, run:

# Download dependencies
go mod download

# Build binary with embedded assets
go build -o postctl main.go

# Run help command to verify
./postctl --help

Configuration Directory

The application creates and expects files in the user config directory: ~/.config/postctl/

  • config.yaml: Holds all your global default preferences, AI configurations, and platform API credentials.
  • postctl.db: The local SQLite database housing campaigns, post statuses, histories, and encrypted credentials.

📝 Vim / Neovim External Editor Flow

Inside the interactive Terminal UI, you can spawn your terminal-native text editor (like Neovim or Vim) to edit post text, adjust templates, or modify metadata on the fly.

Did you know? Any changes to metadata inside the YAML block in Vim will sync back into your TUI fields automatically.

How to trigger the Editor

  1. Start the TUI dashboard: ./postctl tui
  2. Navigate to a post or campaign and press e to open the edit form.
  3. While focused on the body or any text input field, press ctrl+v.
  4. The TUI process suspends, and your terminal launches your configured editor (reads $EDITOR, falling back to nvim or vim).

The Template File Structure

Vim opens a temporary file formatted as Markdown. It contains three distinct blocks:

1. Interactive Helper Block (HTML Comments)

<!--
 postctl Editor Help (TWITTER)
 ==================================
 [Character Ruler (Max 280 characters per tweet)]
 000      030      060      090      120      150      180      210      240      270 280!
 |--------|--------|--------|--------|--------|--------|--------|--------|--------|-|

 Current Thread Status:
   Tweet 1: 220 chars (60 remaining) [✓]
   Tweet 2: 110 chars (170 remaining) [✓]

 NOTE: This helper block will be stripped out automatically upon save.
 Write your post content below this comment:
-->

This block updates dynamically according to your target platform limits (280 for Twitter, 300 for Bluesky, 500 for Mastodon). It is stripped out automatically when saving.

2. YAML Frontmatter Block

---
platform: twitter
campaign: launch-2026
schedule: 2026-06-25 15:00:00
images: ["logo.png"]
---

Bidirectional Syncing: You can edit these values inside Vim. Changing the campaign name, target platform, schedule timestamp, or image list updates the TUI form fields immediately upon return.

3. Content Body

The post body begins after the closing --- frontmatter delimiter. Write your post in standard Markdown.

For multi-post threads, separate each section using a line containing only ---.

🖼️ Image Path Resolution & Platform Uploads

Manage and attach media assets seamlessly using local file references.

How Image Paths Are Resolved

When you reference a file name or path in your post frontmatter (e.g. images: ["dashboard-screenshot.png"]), postctl executes a fallback search in the following sequence:

  1. Absolute Path Check: Is it a literal path on your system? (e.g. /Users/user/Pictures/screenshot.png)
  2. Relative to Markdown Document: If you imported a file from /code/posts/announcement.md, it checks /code/posts/dashboard-screenshot.png.
  3. Current Working Directory (CWD): Checks the folder where you launched the postctl command.
  4. Global Default Image Directory: Checks the directory specified by defaults.image_dir in your ~/.config/postctl/config.yaml file.

Platform-Specific Upload Behavior

Each platform API handles media uploads differently, which is automatically abstracted by the driver layers:

  • LinkedIn (Native Hosting): Images are uploaded as raw binary assets to LinkedIn. The API registers the upload, performs a PUT, and links the resulting URN (e.g., urn:li:digitalmediaAsset:...) to your post.
  • Twitter / X (Native Hosting): Images are uploaded to the Twitter v1.1 Media Upload endpoint to obtain a media_id, which is attached to the v2 tweet.
  • Mastodon / Bluesky (Native): Media files are uploaded to platform attachment endpoints (or blob records for Bluesky AT Protocol) and linked to the status.
  • Threads (Meta HTTPS Requirement): The Threads API does not allow binary local uploads. Instead, it expects a public HTTPS URL (e.g. https://yourdomain.com/assets/screenshot.png) from which the Meta crawler fetches the image. If publishing to Threads, host your images on an external cloud (S3, R2, Imgur) and reference that HTTPS URL in the frontmatter.

Twitter/X API Setup Guide for postctl

This document describes step-by-step how to set up authentication for Twitter/X in postctl.

Two options are available:

  • Option A: Cookie-based Authentication (Free but Unofficial) – Uses your existing browser session. Free, but prone to errors and carries a risk of account suspension.
  • Option B: Official API (Paid & Recommended) – Uses the official Twitter API (requires a paid subscription starting at ~$100/month or prepaid API credits). The secure, stable way.

This method simulates a real browser session by using your login cookies. It is completely free and requires no setup in the Twitter Developer Portal.

[!WARNING] Unofficial Bypass Method (Risk of Account Suspension): Cookie-based authentication simulates a web session. This method is error-prone, violates X/Twitter’s Terms of Service (ToS), and can lead to the suspension of your account. The only official and secure way to post is using the paid API (Option B).

  • postctl attempts to minimize risk by imitating headers and inserting artificial delays (5 seconds between posts), but offers no guarantees.
  • X strictly requires the twid cookie (your User ID). Therefore, always enter the entire cookie string or both cookies (auth_token and ct0).

Step 1: Extract Full Browser Cookies

The easiest way is to copy the entire Cookie header of any request:

  1. Open your web browser, go to x.com, and make sure you are logged in. (We recommend logging out and back in once to refresh the session).
  2. Open Developer Tools (F12 or Cmd + Option + I).
  3. Switch to the Network tab.
  4. Refresh the page (F5 or Cmd + R).
  5. Click on any request to x.com in the network request list (e.g., home or a GraphQL request).
  6. In the right pane under Request Headers, look for the cookie: line.
  7. Copy the entire long value (it usually starts with guest_id=... or kdt=... and contains all cookies).
  8. Additionally, locate the value for ct0 (your CSRF token, ~160 chars long) in the cookies and copy it as well.

Step 2: Set up in postctl

We recommend setting it up via a one-liner in your terminal. Replace the placeholders with your copied values:

./postctl config setup twitter --cookie "YOUR_ENTIRE_COPIED_COOKIE_STRING" --ct0 "YOUR_ONLY_CT0_VALUE"

(Note: If you start the interactive setup via ./postctl config setup twitter and choose option 2, you can also paste the entire long cookie string when prompted for the auth_token).

If a GraphQL error like empty tweet ID returned... or error 226 (This request looks like it might be automated...) occurs when publishing a tweet, postctl automatically falls back to a headless browser flow:

  1. Automatic Browser Start: postctl silently launches Google Chrome in the background (using chromedp), loads your cookies (auth_token & ct0), navigates to the X web interface, populates the composer (including threads and media uploads), and clicks “Post”.
  2. Prerequisite: Google Chrome must be installed on your system (searched by default in /Applications on macOS).
  3. Expired Session Cookies: If the headless browser also fails, your cookies have likely expired. Simply repeat Step 1 and enter the new cookies.

If you own an official developer account and are willing to pay the monthly API costs, you can use the standard OAuth authentication.

[!IMPORTANT] Cost Warning (as of 2026): Twitter/X no longer offers free write access (Free Tier) for newly created developer accounts. To post via the official interface, a paid API access (e.g., Basic Tier for ~$100/month or prepaid credits) is required in the Developer Portal.

Step 1: Configure App in the Developer Portal

  1. Go to the Twitter Developer Portal and log in.
  2. Create a new Project and a new App in your portal dashboard.
  3. In the App Settings, navigate to User authentication settings and click Set up:
    • App Type: Choose Web App, Automated App or Bot.
    • App Permissions: Choose Read and Write (critical for posting rights).
    • Type of App: Choose Native App or Single Page App (for OAuth 2.0 PKCE).
    • Callback URI / Redirect URL: Enter exactly http://localhost:8753/callback.
    • Website URL: Enter your own website or https://github.com/aeon022/postctl.
  4. Save the settings and copy the displayed Client ID and Client Secret to a secure place.

Step 2: Store Credentials

# Set Client ID
./postctl config set twitter.client_id "YOUR_CLIENT_ID"

# Set Client Secret
./postctl config set twitter.client_secret "YOUR_CLIENT_SECRET"

Step 3: Perform Authentication

./postctl auth twitter

A browser window will open asking you to authorize the app. After a successful login, postctl will store your encrypted access and refresh tokens.

LinkedIn API Setup Guide for postctl

This document describes step-by-step how to create a LinkedIn Developer App, migrate it to the modern OpenID Connect (OIDC), and set up your credentials for postctl.


1. Create a Developer App on LinkedIn

  1. Go to the LinkedIn Developer Portal.
  2. Log in with your personal LinkedIn account.
  3. Click on Create App:
    • App Name: Give your app a name (e.g., postctl-publisher).
    • LinkedIn Page: Associate the app with your LinkedIn Company Page or create a temporary one (mandatory field).
    • App Logo: Upload the generated logo from postctl/icons (or any square image).
    • Legal Terms: Accept the terms and click Create app.

2. Add Products (Very Important!)

By default, a new LinkedIn app does not have permissions for posting updates. Since August 2023, LinkedIn replaced the legacy permissions (r_liteprofile). You must activate the following products:

  1. In your app, go to the Products tab.
  2. Search for Share on LinkedIn and click Request access (usually approved instantly).
  3. Search for Sign In with LinkedIn using OpenID Connect (do not choose the outdated Sign In with LinkedIn) and activate it to unlock the modern OIDC login flow.

3. Configure Authentication Settings

  1. Go to the Auth tab.
  2. Scroll to the OAuth 2.0 settings section:
    • Under Authorized Redirect URLs, add the following callback: http://localhost:8753/callback
    • Click Update.
  3. In the OAuth 2.0 scopes section, verify that you see the following scopes:
    • openid, profile, w_member_social, email.

4. Copy Client ID & Client Secret

  1. Stay in the Auth tab.
  2. In the Application credentials section, copy the Client ID and the Client Secret.

5. Configure in postctl

Use the CLI to save the credentials in your config.yaml (use the local binary ./postctl):

# Set Client ID
./postctl config set linkedin.client_id "YOUR_CLIENT_ID"

# Set Client Secret
./postctl config set linkedin.client_secret "YOUR_CLIENT_SECRET"

Verify the settings:

./postctl config show

6. Establish Connection (Auth Flow)

Start the authentication flow:

./postctl auth linkedin
  1. A browser window will automatically open, asking you to authorize your app to access your LinkedIn profile.
  2. After clicking Allow, you will be redirected to the local web server.
  3. The CLI intercepts the token via the OIDC flow, determines your user ID (via /v2/userinfo), encrypts the access token, and saves it in the local SQLite database.
  4. You are now ready to publish posts on LinkedIn!

Threads (Meta) API Setup Guide for postctl

This document describes in detail how to obtain access to the official Threads API, configure your app in the Meta Developer Portal, and set up authentication for postctl.


1. Create a Meta Developer App

  1. Go to the Meta Developer Portal and log in.
  2. Click on My Apps in the top right corner and then Create App:
    • Choose Other as the app type and click Next.
    • Choose Consumer (or any type that allows access to the Threads/Instagram API).
    • Enter a name for your application (e.g., postctl-threads-app).
    • Click Create App (you may be prompted to enter your Facebook password).

2. Add Threads API

  1. In your app dashboard, scroll down to Add Products to Your App.
  2. Find Threads API and click Set Up.
  3. Your app is now in In Development mode – this is perfect and important for local development.

3. Configure Callback URLs (Important: Mandatory Fields & HTTPS!)

Meta strictly enforces secure HTTPS connections for the Threads API. Furthermore, the dashboard will refuse to save changes if deinstallation or deletion callback URLs are missing.

  1. In the left navigation bar, under Threads API, click Threads Settings (or go to Use Cases ➔ Access Threads API ➔ Customize / Settings).
  2. Configure the following three fields:
    • Valid OAuth Redirect URIs: https://localhost:8753/callback (Important: https instead of http!)
    • Uninstall Callback URL: https://localhost:8753/uninstall (or https://example.com/uninstall as a placeholder)
    • Delete Callback URL: https://localhost:8753/delete (or https://example.com/delete as a placeholder)
  3. Click Save in the bottom right.

4. App Roles & Tester Invitation (Important!)

Since your app is in development mode, it can only be authenticated by users registered as testers.

Step A: Add Tester in the Meta Dashboard

  1. Click App Roles (the people icon) ➔ Roles in the left navigation bar.
  2. Scroll to the bottom to the Threads Testers section.
  3. Click Add Threads Testers.
  4. Enter the exact Instagram/Threads username of the account you want to post from, and click Confirm.

Step B: Accept Invitation on Instagram

The invitation must be confirmed manually from your Threads account:

  1. Log in to Instagram.com in a desktop browser using the corresponding account.
  2. Go to your profile ➔ Click the Gear icon (Settings).
  3. Navigate to Apps and Websites in the left menu.
  4. Click on the Tester Invites tab at the top.
  5. You will see the invite from your app (e.g., postctl-threads-app). Click Accept.

5. Retrieve App ID & App Secret

  1. In the left navigation bar, go to App Settings ➔ Basic.
  2. Copy the App ID.
  3. Copy the App Secret (requires clicking Show and entering your password).

6. Configure in postctl

Save your Threads credentials in your terminal (use ./postctl for the local binary):

# Set App ID
./postctl config set threads.app_id "YOUR_APP_ID"

# Set App Secret
./postctl config set threads.app_secret "YOUR_APP_SECRET"

Verify your configuration:

./postctl config show

7. Establish Connection (Auth Flow)

Run the authentication flow:

./postctl auth threads
  1. Your default browser will open and redirect you to the Threads login dialog.
  2. Log in with your Threads account and grant permission to access your profile data and media (threads_basic and threads_content_publish).
  3. After logging in, Meta redirects you back to https://localhost:8753/callback.
  4. Handling the SSL Warning in the Browser:
    • Because postctl’s local server uses a temporary, self-signed SSL certificate for encryption, your browser will display a warning “Your connection is not private” (or NET::ERR_CERT_AUTHORITY_INVALID).
    • This is completely normal and safe, as all communication takes place locally on your Mac.
    • Click “Advanced” (or Show Details) in the browser window and select “Proceed to localhost (unsafe)”.
  5. Once clicked, postctl receives the access token, exchanges it in the background for a long-lived token valid for 60 days, and stores it encrypted in the SQLite database.
  6. A success message will appear in your terminal! You are now ready to post updates to Threads.

🦋 Bluesky & Mastodon Setup Guide

Learn how to connect postctl to Bluesky (via the AT Protocol) and Mastodon instances.

1. Bluesky Integration

Bluesky uses the decentralized AT Protocol. Instead of complex OAuth, it uses secure App Passwords.

Step A: Create an App Password

  1. Log in to your Bluesky account in your browser.
  2. Go to Settings ➔ App Passwords.
  3. Click Create App Password, give it a name (e.g. postctl-cli), and copy the generated password (usually looks like xxxx-xxxx-xxxx-xxxx).

Step B: Configure postctl

Configure your handle and app password:

# Configure handle
./postctl config set bluesky.handle "yourname.bsky.social"

# Configure app password
./postctl config set bluesky.app_password "xxxx-xxxx-xxxx-xxxx"

Step C: Authenticate

./postctl auth bluesky

The CLI will test the connection, fetch your profile, and save the session tokens in your SQLite database. Bluesky posts support up to 300 characters.


2. Mastodon Integration

Mastodon supports federated instances and uses client credentials registration per instance.

Step A: Configure Instance URL

By default, postctl connects to https://mastodon.social. If you are on a different instance, configure it first:

./postctl config set mastodon.instance_url "https://fosstodon.org"

Step B: Authenticate

Run the login workflow:

./postctl auth mastodon
  1. The CLI checks if a client application exists on that instance. If not, it registers a new app named postctl on your instance automatically.
  2. A browser window will open, prompting you to authorize the app.
  3. Log in and click Authorize.
  4. The tokens are retrieved and securely stored in your local DB. Mastodon posts support up to 500 characters.

🤖 Facebook Setup Guide

Configure automated publication to Facebook business/creator Pages using Meta Graph API tokens.

Facebook Integration

Facebook posts are pushed directly to Business/Creator Pages using Meta Graph API tokens.

Step A: Create App on Meta Portal

  1. Go to the Meta Developer Portal.
  2. Create an app of type Business or Other.
  3. Configure the OAuth Redirect URL: http://localhost:8753/callback
  4. Go to Settings ➔ Basic and copy your App ID and App Secret.
  5. Identify the target Facebook Page ID (found in your Facebook Page "About" info).

Step B: Save to Configuration

./postctl config set facebook.app_id "YOUR_FB_APP_ID"
./postctl config set facebook.app_secret "YOUR_FB_APP_SECRET"
./postctl config set facebook.page_id "YOUR_FB_PAGE_ID"

Step C: Run Authentication

./postctl auth facebook

Accept the prompt to authorize managing and publishing pages. The access token is exchanged for a permanent page access token and saved securely.

postctl — Specification

Overview

postctl is a TUI CLI tool written in Go that manages social media postings and blogs across Twitter/X, LinkedIn, Threads, Mastodon, Bluesky, Facebook, Telegram, Discord, Reddit, Dev.to, Hashnode, and Medium. Posts are authored as Markdown files, imported into a local SQLite database, previewed in a terminal UI, and published via platform APIs — immediately or on a schedule.

User Stories

As a solo developer / indie hacker:

  • I want to write posts as Markdown files in my repo
  • I want to preview how a Twitter thread will look before posting
  • I want to schedule posts for optimal timing across time zones
  • I want to post the same content to multiple platforms with one command
  • I want to see what I’ve posted and when
  • I want character count validation before posting (280 chars per tweet)

As an AI assistant (Claude, GPT, Antigravity):

  • I want to create properly formatted Markdown posts
  • I want to trigger posting via CLI commands
  • I want to check post status and history
  • I want to run the full workflow end-to-end without human intervention (except approval)
  • I want structured JSON output for all commands so I can parse results
  • I want dry-run mode to preview everything before committing

AI-as-Operator Principle

postctl is designed for AI to operate, not just to generate text.

Most social media tools treat AI as a copywriting feature — “AI writes your caption.” postctl treats AI as the operator of the entire tool. The human sets the strategy and approves; the AI executes.

Workflow: AI operates, human approves

Human: "Post the new Orbiter v0.3.66 release"
   ↓
AI (Claude/GPT):
   1. Writes posts as Markdown files (all platforms, EN+DE)
   2. postctl import ./posts/
   3. postctl list --format json          → shows draft posts
   4. "Here are 8 posts ready. Review?"
   ↓
Human: "Change the LinkedIn DE version, rest is fine"
   ↓
AI:
   5. Edits the file, re-imports
   6. postctl campaign post orbiter-v0366 --dry-run
   7. "Dry run passed. Post?"
   ↓
Human: "go"
   ↓
AI:
   8. postctl campaign post orbiter-v0366
   9. "Posted. 4/4 Twitter, 2/2 LinkedIn, 2/2 Threads. IDs: ..."

Design requirements for AI operation

  1. All commands must work non-interactively — no prompts, no “are you sure?”, no interactive menus. Flags control everything.
  2. --format json on all commands — structured output that AI can parse. Default is human-readable, --format json returns machine-readable.
  3. --dry-run on all mutation commands — AI can preview without side effects. Human approves, then AI runs without --dry-run.
  4. Exit codes — 0 = success, 1 = validation error, 2 = API error, 3 = auth error. AI reads exit codes, not just output.
  5. Idempotent imports — running postctl import twice doesn’t duplicate posts. AI can re-import after edits without cleanup.
  6. Partial failure recovery — if tweet 3/5 fails, postctl post <id> --resume continues from where it stopped. AI doesn’t need to track state manually.
  7. No browser required — OAuth flow uses localhost callback, but once authenticated, everything is CLI-only. AI never needs to open a browser.
  8. Batch operations — postctl campaign post <name> posts all posts in a campaign. AI doesn’t need to loop over individual posts.

JSON output example

$ postctl list --format json
{
  "posts": [
    {
      "id": "orbiter-v0366-twitter-en",
      "platform": "twitter",
      "type": "thread",
      "status": "draft",
      "tweets": 5,
      "images": 2,
      "chars": [245, 220, 180, 260, 190],
      "valid": true
    }
  ],
  "total": 8,
  "by_status": {"draft": 8, "posted": 0, "scheduled": 0}
}
$ postctl post orbiter-v0366-twitter-en --format json
{
  "ok": true,
  "platform": "twitter",
  "tweets_posted": 5,
  "thread_id": "1234567890",
  "urls": ["https://x.com/gerwinweiher/status/1234567890"]
}

Core Workflows

1. Import Workflow

Markdown files → postctl import → SQLite DB
                                    ↓
                              Posts with status "draft"

Input: Directory of .md files with YAML frontmatter Processing:

  • Parse frontmatter (platform, type, language, campaign, schedule, images)
  • Parse body into tweets (split on ## Tweet N headers)
  • Detect reply section (## Reply)
  • Validate: character count per tweet (≤280), image paths exist
  • Generate deterministic ID from filename + platform
  • Insert/update in SQLite (upsert on ID)

Output: Posts in DB with status draft or scheduled (if schedule: frontmatter present)

2. Preview Workflow

postctl (no args) → TUI
                      ↓
              Dashboard → Post list → Detail view
                                        ↓
                                   Tweet-by-tweet preview
                                   with char count + image indicators

3. Post Workflow

postctl post <id> → Load from DB → Validate → API Call → Update status
                                                ↓
                                          Upload images first
                                          Then post text with media IDs
                                          For threads: post sequentially,
                                          reply to previous tweet ID

Thread posting sequence:

  1. Upload all images → get media IDs
  2. Post Tweet 1 → get tweet ID
  3. Post Tweet 2 as reply to Tweet 1 → get tweet ID
  4. Post Tweet 3 as reply to Tweet 2 → …
  5. Post Reply tweet as reply to last tweet
  6. Update DB: status = “posted”, platform_id = first tweet ID

Error handling:

  • If tweet N fails: mark post as “partial”, store last successful tweet ID
  • Retry: resume from the failed tweet (don’t re-post successful ones)
  • Rate limit hit: wait and retry with exponential backoff

4. Schedule Workflow

postctl schedule <id> "2026-06-23 09:00"
       ↓
  Update DB: status = "scheduled", scheduled_at = datetime
       ↓
  Scheduler daemon picks it up at the right time
       ↓
  Same as Post Workflow

Scheduler:

  • Runs as background goroutine when TUI is open
  • Also runs as postctl daemon for headless mode
  • Checks every 30 seconds for due posts
  • Posts in order of scheduled_at

5. Auth Workflow

postctl auth twitter
       ↓
  Open browser → Twitter OAuth consent page
       ↓
  Local HTTP server on :8753 catches callback
       ↓
  Exchange code for token
       ↓
  Store encrypted token in SQLite

Markdown Format Spec

Frontmatter Fields

Field Required Type Values Default
platform yes string twitter, linkedin, threads, all —
type yes string thread, single, article —
language no string ISO 639-1 (en, de) en
campaign no string freeform slug —
schedule no datetime ISO 8601 local —
images no list relative file paths —
tags no list hashtags without # —

Body Format

Single post (LinkedIn, Threads):

---
platform: linkedin
type: single
---

The entire post body goes here.
Multiple paragraphs supported.

Thread (Twitter):

---
platform: twitter
type: thread
images:
  - screenshots/01-dashboard.png
---

## Tweet 1

First tweet content. No links here for algorithmic reach.

## Tweet 2

Second tweet. Attach image: screenshots/01-dashboard.png

## Tweet 3

Third tweet content.

## Reply

Links and hashtags go in the self-reply.
github.com/aeon022/orbiter

#opensource #webdev

Rules:

  • ## Tweet N splits into individual tweets
  • ## Reply is posted as a reply to the last tweet
  • Image assignment: first image in images: list goes to Tweet 2, second to Tweet 3, etc. Or use <!-- image: filename.png --> inline
  • Character count: ≤280 per tweet (URLs count as 23 chars per Twitter’s t.co)
  • Empty tweets are skipped

Platform API Details

Twitter/X v2

Auth: OAuth 2.0 with PKCE

GET https://twitter.com/i/oauth2/authorize
  ?client_id=...
  &redirect_uri=http://localhost:8753/callback
  &scope=tweet.read+tweet.write+users.read+offline.access
  &response_type=code
  &code_challenge=...
  &code_challenge_method=S256
  &state=...

Post tweet:

POST https://api.twitter.com/2/tweets
Authorization: Bearer <token>
Content-Type: application/json

{"text": "...", "media": {"media_ids": ["..."]}, "reply": {"in_reply_to_tweet_id": "..."}}

Upload media (v1.1 — still required):

POST https://upload.twitter.com/1.1/media/upload.json
Content-Type: multipart/form-data

media_data=<base64>

LinkedIn v2

Post:

POST https://api.linkedin.com/v2/ugcPosts
Authorization: Bearer <token>

{
  "author": "urn:li:person:<id>",
  "lifecycleState": "PUBLISHED",
  "specificContent": {
    "com.linkedin.ugc.ShareContent": {
      "shareCommentary": {"text": "..."},
      "shareMediaCategory": "IMAGE",
      "media": [{"status": "READY", "media": "<asset-urn>"}]
    }
  },
  "visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}
}

Image upload (2-step):

  1. Register: POST /v2/assets?action=registerUpload → get upload URL + asset URN
  2. Upload: PUT <upload-url> with binary image data

Threads (Meta Graph API)

Create container:

POST https://graph.threads.net/v1.0/<user_id>/threads
  ?media_type=TEXT
  &text=...
  &access_token=...

Publish:

POST https://graph.threads.net/v1.0/<user_id>/threads_publish
  ?creation_id=<container_id>
  &access_token=...

Error Handling Strategy

Error Action
Rate limit (429) Wait retry-after header, then retry
Auth expired (401) Attempt token refresh, if fails prompt re-auth
Network error Retry 3x with exponential backoff (1s, 4s, 16s)
Partial thread Mark as “partial”, store progress, allow resume
Invalid content Validation error before API call, show in TUI
Image too large Resize with Go image library before upload

Non-Goals (v1)

  • No web dashboard
  • No multi-user / team features
  • No built-in image generation
  • No automatic cross-posting (explicit per platform)

Success Metrics

  • Import 20 Markdown posts in <1 second
  • Post a 6-tweet thread with 2 images in <10 seconds
  • TUI renders at 60fps on standard terminal
  • Single binary, <20MB, no runtime dependencies
  • Works on macOS, Linux, Windows

Future Features (v2+)

Note: everything in this section is a forward-looking idea, not a commitment or a currently sold product. postctl’s actual pricing today is the one-time missionctl Bundle license (Polar.sh) described on the pricing page — not the subscription tiers sketched out below.

postctl generate

AI generates posts from a URL or a Markdown article.

  • Input: URL, Markdown file, or free text
  • Output: thread draft + LinkedIn post + Threads post as Markdown
  • Uses the Claude API, OpenAI API, or Ollama (local)
  • User reviews and edits before posting

postctl repurpose

Takes an existing post and converts it for other platforms.

  • Dev.to article → Twitter thread + LinkedIn + Threads
  • Twitter thread → LinkedIn long-form post
  • Automatically adapts length, tone, and hashtags

postctl analytics

Fetches engagement data from the APIs after posting.

  • Likes, retweets, impressions (Twitter)
  • Reactions, comments (LinkedIn)
  • Determines best posting times
  • Terminal dashboard with sparklines

postctl template

Pre-built post structures.

  • postctl template launch — product launch announcement
  • postctl template feature — feature update thread
  • postctl template thought — thought leadership post
  • Generates a Markdown file with placeholders

Ecosystem Vision

postctl is part of a content loop:

Orbiter (Create) → postctl (Distribute) → Analytics (Learn) → Orbiter (Improve)
  1. Write content in Orbiter (blog posts, pages)
  2. Export/generate posts as Markdown
  3. postctl distributes to Twitter, LinkedIn, Threads
  4. Analytics shows what’s working
  5. Insights feed into the next content cycle

Long-term: orbiter export --to-postctl as an integration.


Project Setup

Repository: ~/Developing/Projects/postctl
Module:     github.com/aeon022/postctl
License:    MIT

Go Tutorial — postctl

This tutorial guides you through learning Go using the postctl project. Each phase builds a feature and teaches new Go concepts.

Course Concept

This tutorial is written in parallel with the project and can be published as a paid course:

  • Format: Video series (screen recording + voiceover) + written tutorial
  • Title: “Build a Real CLI Tool in Go — From Zero to Production”
  • Target Audience: Developers who want to learn Go by building a real project
  • Teaser: Phase 1 + 2 free as blog posts on Dev.to (traffic + leads)
  • Full Version: Phase 3-7 as a paid course ($49-79)
  • Platform: Self-hosted site (Lemon Squeezy) or Udemy
  • Languages: German (primary) + English

Each phase ends with a Challenge — a task the learner solves themselves before seeing the solution.


Prerequisites

# Install Go
brew install go

# Verify version (1.22+)
go version

# Editor: VS Code + Go Extension (gopls)
code --install-extension golang.go

Phase 1: Hello Go — Project Setup

What we build

Basic structure with Cobra CLI: postctl version, postctl help

Go Concepts

  • Go Modules: Dependency Management (go.mod)
  • Packages: Code organization in folders
  • func main(): Entry Point
  • fmt.Println: Output
  • Strings: String formatting with fmt.Sprintf

Steps

# 1. Create project
mkdir postctl && cd postctl
go mod init github.com/aeon022/postctl

# 2. Install Cobra
go get github.com/spf13/cobra@latest

# 3. Create folders
mkdir -p cmd

main.go — Entry Point:

package main

import "github.com/aeon022/postctl/cmd"

func main() {
    cmd.Execute()
}

cmd/root.go — Root Command:

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

// Version — set at build time
var Version = "dev"

var rootCmd = &cobra.Command{
    Use:   "postctl",
    Short: "Social media posting from the terminal",
    Long:  "postctl manages social media posts from Markdown files.\nTwitter/X, LinkedIn, and Threads — from one CLI.",
}

// Execute — called by main()
func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func init() {
    // Version subcommand
    rootCmd.AddCommand(&cobra.Command{
        Use:   "version",
        Short: "Print version",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Printf("postctl version %s\n", Version)
        },
    })
}

Challenge 1

Add a command postctl hello <name> that prints Hello <name>! Welcome to Go.. (Tip: Check cobra.ExactArgs(1)).


Phase 2: File Reader — Markdown Parser

What we build

A parser that reads a Markdown file, extracts YAML Frontmatter (metadata like platform, schedule, images) and the content body.

Go Concepts

  • Structs: Custom data types for Post/Tweet
  • File I/O: os.ReadFile, os.Open
  • Error Handling: error interface and if err != nil
  • Strings/Slices: strings.Split, strings.HasPrefix
  • Pointers: Passing by reference vs value

Code Example: Metadata Structs

package models

import "time"

type Post struct {
    ID          string
    Platform    string    // twitter, linkedin, threads
    Type        string    // single, thread
    Campaign    string
    ScheduledAt *time.Time
    Body        string
    Images      []string
    Status      string    // draft, scheduled, posted, failed
    Error       string
}

Challenge 2

Write a function ParseFrontmatter(content string) (models.Post, error) that splits the YAML block (between ---) and parses key-value pairs manually without using external libraries.


Phase 3: Local Database — SQLite Integration

What we build

Save imported posts to a local SQLite database. Update post status (draft, scheduled, posted) and save publishing history.

Go Concepts

  • Database Connection: database/sql package
  • Driver: Go SQLite driver (CGO-free via modernc.org/sqlite)
  • SQL Operations: CREATE TABLE, INSERT, UPDATE, SELECT
  • Time Parsing: time.Parse and time zone handling

Challenge 3

Create a database table posts and write a method SavePost(post models.Post) error that inserts a new post or updates an existing one if the ID already exists (UPSERT).


Phase 4: API Clients — Publishing to Platforms

What we build

HTTP client implementations for Twitter/X, LinkedIn (OIDC), and Threads API.

Go Concepts

  • Interfaces: Define a Platform interface that all clients implement:
    type Platform interface {
        Auth(ctx context.Context) error
        Post(ctx context.Context, post *models.Post) (string, error)
        IsAuthenticated(ctx context.Context) bool
    }
  • HTTP Requests: net/http package, custom headers, JSON payloads
  • OAuth 2.0 Flow: Token exchange and local web callback server

Challenge 4

Implement the LinkedIn client using the openid and w_member_social scopes to publish a simple status message.


Phase 5: Browser Automation — Headless Fallback

What we build

A headless browser fallback for X/Twitter when API calls fail, utilizing chromedp to set login cookies, navigate the web interface, type tweets, and post them.

Go Concepts

  • Context: Timeout and cancellation propagation
  • Goroutines & Channels: Network response interception to capture GQL tweet IDs
  • DevTools Protocol: Driving Chrome via CDP

Challenge 5

Set Chrome flags to bypass automated browser checks (e.g., hiding navigator.webdriver via AutomationControlled).


Phase 6: Terminal User Interface (TUI) — Bubble Tea

What we build

An interactive terminal dashboard with tabs (Dashboard, Posts, Schedule, Settings) showing campaigns, posts, and scheduler status.

Go Concepts

  • Elm Architecture: Model-Update-View pattern in CLI
  • Bubble Tea Framework: tea.Model, tea.Msg, tea.Cmd
  • Terminal Rendering: lipgloss for styling, borders, and layouts

Challenge 6

Build a custom text input field that opens an external Vim/Neovim editor, suspends the TUI, and restores the terminal state upon editor exit.


Phase 7: Background Scheduler — Daemon

What we build

A background daemon (postctl daemon) that runs in the background, checks the database every 10 seconds, and publishes due posts automatically.

Go Concepts

  • Ticker: time.NewTicker for periodic tasks
  • Signals: Catching OS signals (SIGINT, SIGTERM) for graceful shutdown
  • File Locks: Prevent starting multiple scheduler processes concurrently

Challenge 7

Implement a locking mechanism using a pidfile (postctl.pid) to ensure only one daemon runs at a time.