Post to social networks from Saltcorn via Postiz.
Find a file
2026-06-17 17:40:57 -05:00
lib Initial commit. 2026-06-17 17:40:57 -05:00
test Initial commit. 2026-06-17 17:40:57 -05:00
.gitattributes Initial commit. 2026-06-17 17:40:57 -05:00
.gitignore Initial commit. 2026-06-17 17:40:57 -05:00
index.js Initial commit. 2026-06-17 17:40:57 -05:00
package.json Initial commit. 2026-06-17 17:40:57 -05:00
README.md Initial commit. 2026-06-17 17:40:57 -05:00

postiz-poster

A Saltcorn plugin that publishes table rows to social networks through a Postiz instance -- self-hosted or Postiz Cloud. Postiz handles the per-platform fan-out (Mastodon, Bluesky, X, LinkedIn, Facebook, Instagram, Threads, Telegram, and more); this plugin just maps a Saltcorn row into a Postiz post and calls Postiz's public API.

How it works

  • Plugin config holds the Postiz connection: a base URL and a public API key. Self-hosted and cloud use the same /public/v1 API, so switching between them is only a base-URL change -- no code change.
  • One action, post_to_postiz, is attached to any trigger (row Insert / Update, or a button). When it fires it reads the post text (and an optional media URL) from the row, optionally uploads the media to Postiz, and creates the post against the channels you selected.
  • The channel picker in the action config is populated from the live Postiz instance (GET /public/v1/integrations), so admins choose real connected accounts instead of pasting ids. If Postiz is unreachable at config time the field degrades to a free-text id list.

Layout

postiz/
  index.js              plugin entry: wires config workflow + the action
  lib/
    constants.js        plugin name/version, Postiz endpoints, post types
    postizClient.js     the only module that speaks HTTP to Postiz
    config.js           configuration_workflow (base URL + API key)
    action.js           post_to_postiz: configFields, payload builder, run
    secretsAtRest.js    AES-256-GCM seal/open for the API key (shared helper)
  test/
    e2e.js              payload + secrets gates, plus optional live gate

Secrets at rest

The Postiz API key is configured through the normal plugin UI but is not left plaintext in the database. Saltcorn stores plugin configuration unencrypted in _sc_plugins.configuration, which would otherwise expose the key to DB backups, admin reads, and dev-deploy's journal sync. Instead:

  • lib/secretsAtRest.js seals the key with AES-256-GCM under a key derived (HKDF-SHA256) from Saltcorn's session_secret -- which lives in the env / config file, not the database. Ciphertext in the DB is useless without it.
  • Because Saltcorn's config form autosaves raw values, onLoad re-seals any plaintext key the moment the plugin (re)loads after a save; the action decrypts only at the point of use.
  • Threat model: this protects DB-only exposure (backups, sync, config-table reads). It does not protect a host that leaks both the database and the env / config file. Rotating session_secret invalidates the sealed key -- you must re-enter it in the plugin config (the action raises a clear error saying so).
  • For the same reason, the sealed key is environment-specific: it will not decrypt on a peer with a different session_secret, so exclude it from any dev-deploy sync and set it per environment.

Configuring

  1. In Postiz, connect your social accounts, then create a key under Settings -> Developers -> Public API.
  2. In Saltcorn: Settings -> Plugins -> postiz-poster, set:
    • Postiz base URL -- self-hosted https://postiz.example.com, or cloud https://api.postiz.com.
    • Public API key.
  3. Create a trigger on the table you want to publish from, choose action post_to_postiz, then pick the content field, optional media field, target channels, and timing (now / schedule / draft).

Testing

node test/e2e.js                                   # pure payload gate only
POSTIZ_BASE_URL=http://localhost:5000 \
POSTIZ_API_KEY=xxxxxxxx node test/e2e.js           # + live gate vs a real Postiz

The live gate lists integrations and creates a draft (safe to delete) using the first connected channel. With no env vars it is skipped, not failed.

To confirm before production

These are sketched against the documented Postiz API and isolated to one or two spots each, but verify against docs.postiz.com/public-api, which has changed across Postiz's weekly releases:

  • Create-post body shape (type / date / posts[].integration / value[].content / value[].image) -- see buildPostPayload in lib/action.js.
  • Auth header format -- bare key vs Bearer (AUTH_HEADER in lib/constants.js).
  • Media upload -- whether /upload accepts a URL fetch or wants multipart, and the id/path field it returns (uploadMedia in lib/postizClient.js).

Notes

  • Postiz removes the subscription cost (self-hosted) but not the gatekeeping: you still register your own developer apps with each walled-garden platform (Meta App Review, LinkedIn Community Management API, separate Threads app) and own the OAuth token upkeep. Mastodon / Bluesky / X are straightforward.
  • Underlying platform fees still pass through (e.g. X per-post pricing). Postiz brokers the call; it does not make any platform's API free.