sc-postiz-poster/README.md
2026-06-17 17:40:57 -05:00

104 lines
4.9 KiB
Markdown

# postiz-poster
A Saltcorn plugin that publishes table rows to social networks through a
[Postiz](https://github.com/gitroomhq/postiz-app) 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](https://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.