Skip to content

Payload CMS Adapter

The @smta/payload adapter wires SMTA’s auth interface to Payload CMS’s session context via a PostgreSQL session variable, allowing SMTA to run alongside Payload without interfering with Payload’s own data layer.

FilePurpose
roles.sqlOptional role-membership wiring — grants app_user/app_admin to your login roles via the smta.app_login_role / smta.admin_login_role GUCs
auth_payload_impl.sqlImplements core.get_current_user_id() via the app.current_user_id session variable
secrets_pgcrypto_impl.sqlpgcrypto-backed secrets (store/read/delete) keyed off the app.secrets_key GUC
CREATE OR REPLACE FUNCTION core.get_current_user_id()
RETURNS UUID AS $$
SELECT NULLIF(current_setting('app.current_user_id', true), '')::UUID;
$$ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = core, public;

Unlike Supabase, Payload does not set a database-level session variable automatically. Your application middleware must set it using the injectUserContext helper from @smta/payload:

import { injectUserContext } from '@smta/payload'
// Call before any SMTA-guarded query within the same transaction
await injectUserContext(db, userId)

The helper uses set_config with a parameterized query — safe from SQL injection:

export async function injectUserContext(db: DbExecutor, userId: string): Promise<void> {
await db.query(`SELECT set_config('app.current_user_id', $1, true)`, [userId])
}

The Payload adapter ships a pgcrypto-backed secrets implementation (secrets_pgcrypto_impl.sql), so core.store_secret_impl(), core.read_secret_impl(), and core.delete_secret_impl() are fully implemented. Secret values are encrypted with pgp_sym_encrypt into core.encrypted_secrets and referenced by a generated UUID.

The symmetric key is read from the app.secrets_key GUC, which your backend must set per session/connection — it is never hard-coded in SQL. If app.secrets_key is unset, storing or reading a secret raises an exception.

-- Set once per connection, before calling the secrets functions
SELECT set_config('app.secrets_key', '<your-encryption-key>', false);

Payload CMS manages its own tables in the public schema. SMTA uses core, platform, utils, and public (functions only — no tables). These schemas do not overlap. Payload’s collections and SMTA’s tenant infrastructure coexist without conflict.

From your Payload collection hooks or route handlers:

const { rows } = await db.execute(
sql`select * from public.list_my_organizations()`
)
Terminal window
npm run build:payload # or: npx @smta/cli --adapter payload
# → output/SMTA-payload-<timestamp>.sql (60 files)