User Manual · 07 · Security & Compliance
WholeReach — Automated Marketing System

Security, Privacy & Compliance

Secrets handling, access control, data ownership, platform policy, disclosure law, backups, and disaster recovery.

A marketing engine that posts on your behalf earns trust in two directions: it has to protect the keys to your accounts, and it has to be honest about what it says. This page covers both — how secrets, access, and data are handled, how the system recovers from trouble, and the compliance basics worth keeping in mind when you run it for clients.

Security posture at a glance. Access to the publishing hub is SSH-key only, and public sign-up is switched off once the owner account exists. Platform secrets live in a single root-only file (chmod 600), installed from the owner's own terminal and never through a chat or an assistant. Access tokens sit in the hub database, not in code. The private approval links are HMAC-signed and expire. Everything is served over HTTPS. A nightly backup is copied off the box. And a hard economic guardrail — flat subscription, never per-token AI billing — means no runaway bill can be run up against the system.

Overview — what we protect and why

This system holds two things worth guarding. The first is the set of credentials that let it post to your social accounts — the X developer keys, the Bluesky app-password, and any platform you connect later. The second is the data that flows through it: the verified facts each post is built from, the queue of drafts, and the record of what was published for whom. Everything on this page is about keeping those two categories safe, private, and recoverable.

The design principle underneath all of it is simple: keep secrets in as few places as possible, make those places hard to reach, and make sure that if something does leak, rotating it is a short, known procedure rather than a scramble. The rest of this page walks through each layer — secrets, access, data ownership, compliance, backup, incident response, the cost guardrail, and the audit trail — in plain terms, with the technical detail tucked into "Under the hood" notes where it helps.

600
secrets file mode
SSH
key-only access
HMAC
signed magic links
HTTPS
everywhere
04:17
nightly off-box backup
$0
per-token AI billing

Secrets handling

Every platform you connect requires a secret of some kind — a developer app's client ID and secret, an app-password, an OAuth token. The rule this system follows is that those secrets live in exactly one place on disk, that place is readable only by root, and they get there without ever passing through a conversation.

One file, locked down

Platform keys live in /opt/postiz/social.env on the hub. That file is set to chmod 600 — readable and writable by root only, invisible to any other user on the box. It is not in a code repository, not in the portal directory that gets served to the web, and not copied around. One file, one owner.

Installed from your own terminal, never through chat

Keys are put in place by small installer scripts — set-x-keys.sh, set-linkedin-keys.sh, set-meta-keys.sh — that you run yourself over SSH. For example:

ssh -t root@64.227.29.192 /opt/postiz/set-x-keys.sh
# prompts you for the keys in YOUR terminal, writes them to social.env

This matters. The secret is typed into your terminal session and written straight to the locked file. It never appears in a chat window, a support ticket, an assistant's context, or a log. An assistant can tell you which script to run; it never sees or handles the key itself.

Why a script instead of "just edit the file"? The installer writes the value in the exact format the app expects and keeps the file's permissions correct, so there's no chance of leaving it world-readable by hand. It's the safe path, and it's the same path for every platform.

Access tokens live in the database, not the code

Once a platform is connected, the ongoing access token and refresh token are stored in the hub's database — not in source files. They sit in the Postiz Integration table alongside the account name and provider. Because they live in the DB, they travel with the nightly backup: restore the database and your connections come back with it (see Backup & disaster recovery).

Under the hood

The Integration table columns include providerIdentifier, name, token, refreshToken, internalId, disabled, deletedAt, and organizationId. Keeping tokens here — rather than in social.env or any script — is what lets a full rebuild restore working connections from a single database dump.

If a key is ever exposed — the rotation procedure

Secrets leak in ordinary ways: a screenshot, a pasted log, a shared credential. The fix is quick and always the same two steps:

  1. Rotate at the platform. Go to the provider's developer console (for X that's console.x.com) and regenerate the affected key or token. The old value is now dead — anything that leaked is worthless.
  2. Re-install the new value. Re-run the same installer script (set-x-keys.sh and friends) from your own terminal to write the fresh key into social.env.
Rotate first, then re-install. Always invalidate the leaked key at the platform before anything else. Re-installing the new key without killing the old one leaves the exposed one live. Kill it at the source, then update the box.

Access control

The fewer doors into a system, the fewer to guard. This one keeps the count low on purpose.

SSH-key only

You reach the hub over SSH with a key — not a password. Password login is the classic weak point for an internet-facing server; removing it removes the entire category of guess-the-password attacks. If your key isn't on the box, you don't get a shell.

Public sign-up is disabled

The publishing app supports self-service account creation, which is exactly what you do not want on a private hub. Once the owner's account exists, registration is switched off with DISABLE_REGISTRATION=true. No stranger can create an account on your hub, period.

Approval links are signed and expiring

Clients approve posts through a private, phone-friendly link rather than a login. Those links are not guessable URLs — each one carries an HMAC-signed, expiring token. The signature means a link can't be forged or tampered with, and the expiry means an old link stops working on its own. A tenant gets a fresh link generated on demand:

portal_db.make_token(slug, days)
# → https://postiz.wholetech.com/portal/app/?c=<slug>&t=<token>

See Managing Clients for how these links fit the client workflow, and the approval playbook for the step-by-step.

HTTPS everywhere

Both servers run nginx with free Let's Encrypt certificates managed by certbot, so every connection — the hub UI, the API, the approval links — is encrypted in transit. This isn't only about eavesdropping: the login cookie is set secure, so logging in over plain http simply won't stick. Always use https://postiz.wholetech.com.

The most common "I can't log in." Nine times out of ten it's an http:// address. Switch to https:// and the cookie holds. Noted here because it's a security feature doing its job, not a bug.

Data ownership & privacy

When you run this for a client, their data is theirs. The architecture keeps it that way by isolating each tenant and by storing as little as the job requires.

Per-tenant isolation

A client is a tenant — one block in clients.json — with their own brand, cadence, channels, templates, and their own verified-facts file at listings/<tenant>.json. One tenant's posts are drawn only from that tenant's facts and sent only to that tenant's connected accounts. Nothing bleeds across. The owner's own network runs as just another tenant (wholetech) alongside everyone else, on the same rules.

What's stored — and what isn't

StoredNot stored
The tenant's verified facts the posts draw from (listings/<tenant>.json)Any private data the tenant didn't put in their facts file
The draft/approved/published post queue (portal.db)Passwords — access is by SSH key and signed link, not stored credentials
Connected-account access tokens (in the hub DB)The platform keys themselves, which stay in the root-only social.env, separate from tenant data
A log of every approve / skip / publish actionPer-post AI prompts or model calls — there are none; posts are template-built

Because the content engine is deterministic and template-based, there is no third-party AI service receiving your data to generate posts — the facts stay on your own servers.

Public privacy & terms, and a data-deletion path

A published privacy policy and data-deletion route already exist at automarketingengine.com/privacy/ (with a #data-deletion section) — the same policy the Meta app review points to. That gives clients and platforms a real, findable statement of what's collected and how to have it removed.

How to fully remove a client

Off-boarding a tenant is a clean, complete operation, not a half-measure: disconnect their channels in the hub so no further posts can go out, remove their block from clients.json so nothing is generated for them, and delete their facts file and their rows from the queue. Their access tokens are removed with the disconnect. See Managing Clients for the tenant lifecycle end to end.

Compliance & platform policy

When a machine posts publicly on behalf of a brand, a handful of ordinary rules apply. None of this is legal advice — it's a plain-English checklist of responsibilities to keep in view and, where needed, to run past your own counsel. Think of the table below as "things a responsible operator stays on top of."

AreaWhat it meansYour responsibility
FTC endorsement disclosurePaid, sponsored, or affiliate content has to be disclosed as such to the audience.Make sure any post that promotes a paid or affiliate relationship says so clearly.
Platform API TermsEach network (X, Bluesky, LinkedIn, Meta, and the rest) has its own developer/API terms governing automated posting.Connect through the proper developer app and stay within each platform's terms and rate limits.
CAN-SPAMThe U.S. law governing commercial email — honest headers, no deception, a working unsubscribe.Applies to any email the operation sends; the social engine itself doesn't email, but keep it in mind for anything that does.
Data-deletion requestsPeople and platforms can ask that their data be removed.Honor them — the published privacy policy and the client-removal procedure above are how you do it in practice.
Framing. These are responsibilities, not features of the software, and they're deliberately stated generally. The system gives you the tools to meet them — disclosure is up to how you word posts, proper platform apps keep you within API terms, and the privacy policy plus removal path cover deletion — but the obligations belong to the operator. When in doubt on specifics, ask a lawyer.

Backup & disaster recovery

The question that matters isn't "will something break," it's "how fast are you back." The answer here is: everything that matters is backed up nightly and off the box, and rebuilding the hub is a short, known sequence.

The nightly backup

On the hub, backup.sh runs every night at 04:17 UTC and captures the three things a rebuild needs: the database (which includes your connected-account tokens), the portal directory (/opt/portal/ — tenant config, templates, facts, the queue), and the config. That archive is copied off the box to cloud object storage, so losing the whole server doesn't lose the backup. The main server — which holds the 190-site network and the source data — keeps its own separate backup.

Rebuilding the hub from scratch

If the hub is lost entirely, the outline is:

1 — provision
Stand up a fresh box. A new droplet on the same specs (Ubuntu, 4GB/2vCPU) with your SSH key.
2 — restore the stack
Bring back Postiz. Restore the Docker stack — the Postiz app, PostgreSQL, Redis, and Temporal — from the backup and config.
3 — restore the portal & DB
Bring back your data. Restore the /opt/portal/ directory and the database. Because tokens live in the DB, your connections come back with it — no re-authorizing every platform.
4 — re-point DNS
Send traffic to the new box. Point postiz.wholetech.com at the new IP, let certbot issue a fresh certificate, and you're live again.

Full command-level recovery steps live in Troubleshooting & Recovery.

Lesson learned — keep OS updates manual. Automatic operating-system updates can trigger an unattended reboot, and a reboot in the middle of the night can interrupt a run mid-publish. Keep OS updates manual so you choose when the box restarts — never mid-run. It's a small setting that prevents a confusing, self-inflicted outage.

Incident response

When something goes wrong — a bad post, a leaked key, a platform acting up — the response follows four steps, in order. Contain first, then figure out what happened, then fix the root cause, then resume.

1 — contain
Hit the kill switch. touch /opt/portal/PAUSE and the publisher sends nothing — every tenant, every channel, frozen instantly. Stopping the bleeding is always the first move.
2 — diagnose
Read the logs and the alert. The system pushes failures to a phone channel via ntfy, and the logs (portal-gen.log, portal-pub.log, and the rest) tell you what happened. Work from Troubleshooting.
3 — rotate
Rotate keys if credentials are involved. If a secret was exposed, run the rotation procedure — kill it at the platform, re-install the new value (see Secrets handling).
4 — resume
Lift the pause. Once the cause is fixed, rm /opt/portal/PAUSE and publishing picks back up on its normal schedule.
The kill switch is your safety net. touch /opt/portal/PAUSE freezes all publishing; rm it to resume. Learn it before you let anything run auto-fire — it's the one command that buys you time to think. Full detail in the stop-everything playbook.

The cost guardrail as a security control

Economics is a security property too. A system that bills per token is a system where a bug, a loop, or an abusive actor can run up a ruinous bill — the software equivalent of leaving the tap open. This engine is built so that can't happen.

The content engine is deterministic and template-based: posts are assembled by filling verified fields into fixed sentence patterns, with no per-post AI call. Generating one post and generating a thousand cost the same — nothing. The whole system runs on a flat $24/month for the hub, and it is a hard rule that it never uses per-token AI billing. There is no metered service in the loop to run away with your budget.

The one paid channel is X, at roughly a cent and a half per post, and even that is capped: publish_due.py enforces X_DAILY_CAP = 3 paid X posts per day. Bluesky is free and uncapped. So the maximum daily spend the machine can create on its own is bounded and known in advance — there's no scenario where it quietly costs ten times what you expected.

No runaway bill is possible. Flat subscription, deterministic generation with zero per-token billing, and a hard daily cap on the only paid channel. The worst case is a known, small number — which is a security guarantee as much as a financial one.

Auditability

Trust in an automated system comes partly from being able to look back and see exactly what it did. This one keeps a record.

The portal logs every approve, skip, and publish action. Whether a post went out automatically, was approved by a person tapping the queue, or was skipped, there's a durable record of it — who did what and when. Combined with the honesty guard (which holds any post it can't back with a verified fact) and the nightly digest from report.py, you always have an answer to "what did it post, and why." Nothing happens off the books.

Under the hood — a hard-won detail. The audit log is written carefully: the post's state is committed to portal.db before the audit entry is written. An earlier version had a second connection writing mid-transaction, which caused a database lock and a 502. That's fixed and shouldn't be reintroduced — the ordering is deliberate. If you ever see a "database is locked" error, that's the area to look at (see Troubleshooting).

The whole picture. Keys locked to root and installed from your own terminal, key-only access with sign-up disabled, signed and expiring approval links, HTTPS throughout, per-tenant data isolation, nightly off-box backups with a known rebuild, a four-step incident drill, a cost model that can't run away, and a log of every action. Security here isn't a bolt-on — it's the reason this engine is safe to hand your brand, or your client's.