Herald/Documentation
← All documentation
Reference

Herald — Data Model (Neon Postgres)

All customer-owned tables carry tenant_id. All timestamps are timestamptz (UTC). Use uuid primary keys (gen_random_uuid() — enable pgcrypto). Tokens are stored encrypted (ciphertext in a text/bytea column); the schema shows the logical field, encryption is applied in the app layer.

Enums

  • network: instagram | linkedin (extensible).
  • account_status: active | expired | revoked.
  • post_status (derived, may be stored denormalized): draft | scheduled | publishing | published | partial | failed.
  • target_status: pending | publishing | published | failed.
  • api_key_status: active | revoked.

Tables

tenants

columntypenotes
iduuid pk
nametext
created_attimestamptzdefault now()

users

columntypenotes
iduuid pk
tenant_iduuid fk→tenants
emailcitext unique
password_hashtextargon2/bcrypt; null if magic-link only
nametext
created_attimestamptz

api_keys

columntypenotes
iduuid pk
tenant_iduuid fk→tenants
nametextlabel
key_prefixtextfirst chars shown in UI (e.g. hld_live_ab12)
key_hashtextsha256 of the full key; full key shown once
statusapi_key_statusdefault active
last_used_attimestamptznull
created_attimestamptz
Index: (key_hash) unique; lookups by hash.

platform_apps

Herald's own registered apps (one row per network). Not tenant-scoped.

columntypenotes
networknetwork pk
client_idtext
client_secrettextencrypted
scopestext[]requested scopes
api_versiontextpinned platform API version
updated_attimestamptz
(Alternatively hold these in env vars; a table lets you rotate without redeploy.)

connected_accounts

columntypenotes
iduuid pk
tenant_iduuid fk→tenants
networknetwork
platform_account_idtextIG user id / LinkedIn org URN
display_nametext
avatar_urltext
page_idtextIG's linked FB Page id / LinkedIn org URN
access_tokentextencrypted
refresh_tokentextencrypted, nullable
token_expires_attimestamptznullable
scopestext[]
statusaccount_statusdefault active
connected_attimestamptz
updated_attimestamptz
Unique: (tenant_id, network, platform_account_id).

oauth_states

Short-lived, single-use CSRF/state for the OAuth dance.

columntypenotes
statetext pksigned random
tenant_iduuid
networknetwork
redirect_uritext
created_attimestamptz
consumed_attimestamptznull until used
(TTL-clean rows older than ~15 min.)

media_assets

columntypenotes
iduuid pk
tenant_iduuid fk→tenants
kindtextimage/video
storage_keytextobject-store key
public_urltextpublic HTTPS URL
bytesbigint
width / heightintnullable
created_attimestamptz

posts

columntypenotes
iduuid pk
tenant_iduuid fk→tenants
created_bytextapi_key id or user id
texttextcaption/commentary
mediajsonblist of {media_id?
scheduled_attimestamptznull = publish now
statuspost_statusderived/denormalized
external_reftextconsumer's idempotency/reference key, nullable
created_attimestamptz
Index: (tenant_id, created_at); unique (tenant_id, external_ref) when not null
(idempotency).

post_targets

One row per (post × connected account). This is the unit the worker publishes.

columntypenotes
iduuid pk
post_iduuid fk→posts
tenant_iduuiddenormalized for scoping
connected_account_iduuid fk→connected_accounts
networknetwork
statustarget_statusdefault pending
scheduled_attimestamptzcopied from post; the worker claims by this
attemptsintdefault 0
platform_post_idtextset on success
permalinktextset on success
error_codetexton failure
error_messagetexton failure
published_attimestamptzon success
locked_attimestamptzworker claim marker
Index: partial on (status, scheduled_at) where status='pending' for the
worker's due-scan; (post_id).

audit_log (optional but recommended)

columntypenotes
iduuid pk
tenant_iduuid
actortextapi_key/user id
actiontexte.g. post.create, account.connect
metajsonbnever tokens
created_attimestamptz

Worker claim query (safe concurrency)

UPDATE post_targets
SET status='publishing', locked_at=now(), attempts=attempts+1
WHERE id IN (
  SELECT id FROM post_targets
  WHERE status='pending' AND scheduled_at <= now()
  ORDER BY scheduled_at
  FOR UPDATE SKIP LOCKED
  LIMIT 20
)
RETURNING *;

Migrations

Use Alembic (or plain numbered SQL under backend/app/db/migrations/). Seed platform_apps (or read from env). Enable pgcrypto (for gen_random_uuid) and citext in the first migration.