This issue tracks all divergences between this fork and postalserver/postal. It is kept as a living document.
Upstream base: postalserver/postal main at d038eaa (v3.3.7, 2026-06-03)
Upstream PRs merged into this fork
| PR |
Title |
Category |
| #3050 |
Fix invalid YAML syntax in config docs |
docs / bug |
| #3130 |
PKCE support for OIDC |
feat ✅ adapted (optional per-provider) |
| #3290 |
Add message headers to webhook event payload |
feat |
| #3488 |
Message-ID idempotency support in send API |
feat |
| #3512 |
Add X-Envelope-From header to incoming messages |
fix |
| #3546 |
Scheduled task to unlock stale webhook request locks |
fix |
| #3568 |
Prevent nil description 500 in routes view |
bug |
| #3570 |
Bound outbound delivery timeouts |
fix |
| #3574 |
Non-interactive make-user via POSTAL_INITIAL_USER_* env vars |
feat |
| #3594 |
Encode subject to UTF-8 before database insertion |
bug |
Upstream PRs evaluated but not merged
| PR |
Title |
Reason skipped |
| #3030 / #3029 |
DKIM changes |
Needs more testing |
| #3031 |
Dequeue size config |
Low priority |
| #3036 / #2855 |
Bounce token improvements |
Not blocking |
| #3074 |
Config hint improvements |
Minor UX |
| #3221 |
Login attempt logging |
Nice-to-have |
| #3399 |
Subject search |
Low priority |
| #3400 |
Listmonk / bounce type |
Niche feature |
| #3446 |
Server priority field |
Schema change, deferred |
| #3467 |
gem.coop source |
Infrastructure |
| #3481 |
OIDC auto-create users |
Superseded by our own JIT provisioning |
| #3516 |
ARM64 Docker image |
Not needed (amd64-only cluster) |
| #3517 |
Raw message retention config |
Deferred |
| #3541 |
JSON structured logging |
Planned |
| #3552 / #3554 / #3557 / #3576 / #3580 / #3582 / #3587 / #3588 / #3589 |
Dependabot bumps |
To be handled via bundle update on next release |
| #3555 |
SMTP relay auth |
Planned |
| #3571 |
Send limit priority |
Deferred |
Fork-specific features
Admin REST API v2
Full CRUD REST API mounted at /api/v2/ (JWT Bearer auth via Keycloak service account):
| Endpoint |
Description |
GET/POST /api/v2/organizations |
List / create organizations |
GET/DELETE /api/v2/organizations/:slug |
Get / delete an organization |
POST/DELETE /api/v2/organizations/:slug/members |
Add / remove members |
GET/POST /api/v2/users |
List / create users |
GET /api/v2/users/:email |
Get user by email |
Auth: Keycloak service account (client_credentials) issues JWT; Postal validates azp claim → super-admin access. Config (api.enabled is required — routes are only registered when it is true):
api:
enabled: true
required_azp: <your-service-account-client-id>
PostgreSQL support (main_db + message_db)
Both databases can be switched to PostgreSQL via config:
main_db:
adapter: postgresql # or mysql2 (default)
host: <pg-primary-host>
port: 5432
# Optional read replica:
ro_host: <pg-replica-host>
message_db:
adapter: postgresql
database: postal_messages # host DB; per-server schemas inside
main_db: Rails multi-DB with RW primary + optional RO replica (connects_to)
message_db: per-server PG schemas ("postal-server-{id}") instead of MySQL databases
Provisioner: dual MySQL/PG path; handles composite primary keys and prefix-length hints correctly
database.yml rewritten: plain ERB interpolation, no YAML anchors/merge-keys (Psych 5 compat), no -%> (ERB 6/Prism compat)
Multi-provider OIDC with optional PKCE (adapted from postalserver#3130)
Multiple OIDC providers configurable under oidc.providers[] (array). Each provider can have independent settings. PKCE is optional per provider:
oidc:
providers:
- id: keycloak
issuer: https://your-keycloak/realms/your-realm
identifier: postal
secret: "..."
pkce: true # optional, default false
Single-provider flat config (oidc.issuer, oidc.pkce) still supported for backwards compat.
Keycloak OIDC + SCIM
- SSO via Keycloak (
postal client, confidential)
- Back-Channel Logout:
POST /auth/oidc/backchannel_logout — invalidates all active sessions for the user
- RP-Initiated Logout: redirects to IdP
end_session_endpoint on sign-out
auto_provision_org: true — JIT org creation on first SSO login
- Keycloak service account: issues
client_credentials JWT for API v2 super-admin access
Image registry
CI publishes to ghcr.io/startuppack/postal:latest (GitHub Container Registry, private org package) on every green main.
Bug fixes (this fork, vs. vanilla Postal)
| Commit |
Fix |
4b5bb7f |
config/initializers/omniauth.rb: add require "postal/oidc_providers" — file not eager-loaded at boot time caused NameError: uninitialized constant Postal::OIDCProviders |
fe05672 |
Rename all OidcProviders → OIDCProviders across 5 files — Rails inflect.acronym "OIDC" causes Zeitwerk to expect OIDCProviders, not OidcProviders |
1877937 |
Rename OidcLogoutController → OIDCLogoutController in oidc_logout_controller.rb — same Zeitwerk acronym inflection requirement |
9d2d721 |
Rename module Api → module API in all app/controllers/api/v2/ files — Zeitwerk expects the constant to match the directory name api/ → API |
2793959 |
app/views/sessions/new.html.haml: rename Postal::OidcProviders → Postal::OIDCProviders — missed by fe05672, caused 500 on login page |
f09c937 |
CI: exclude spec/lib/dns_resolver_spec.rb from rspec — live DNS lookups against dnstest.postalserver.io are flaky in CI environments |
adeda20 |
CI: publish amd64-only — drops QEMU arm64 cross-build (not needed, cluster is amd64); build time 40 min → 5 min |
123ed60 |
message_db/connection.rb insert(): rescue PG::UndefinedColumn when appending RETURNING id — the migrations table has version as PK (not id); retry as plain INSERT |
93d9fa3 |
message_db/provisioner.rb create_table_query_pg: prefix all PG index names with {table_name}_ — PG index names are schema-scoped (unlike MySQL where they are table-scoped), causing PG::DuplicateTable when two tables share the same index name (e.g. on_message_id) |
b7e1c5a |
message_db/provisioner.rb: add portable add_column, add_index, modify_column public methods; rewrite migrations 11–20 that used raw MySQL DDL (backtick quoting, USING BTREE, CONVERT TO CHARACTER SET utf8mb4) to use these helpers |
7716f73 |
message_db/provisioner.rb PG_TYPE_MAP: add tinyint (no width) → smallint mapping — MySQL Mysql2 gem returns tinyint(1) as boolean but bare tinyint as integer (0/1); PG must match. Migration 17 parsed column regressed to tinyint(1) (→ boolean), fixed back to tinyint DEFAULT 0 (→ smallint) |
fd06996 |
message_db/migrations/19_convert_database_to_utf8mb4.rb: guard with return if @database.provisioner.postgresql? — charset conversion (CONVERT TO CHARACTER SET utf8mb4) is a MySQL-only operation; PG is always UTF-8 |
Known divergences from upstream (CI)
db:schema:load instead of db:migrate in CI (bypasses Authie gem unversioned migration)
spec/senders/smtp_sender_spec.rb: timeout context uses /timeout/ regex (upstream says error)
spec/lib/worker/jobs/process_queued_messages_job_spec.rb: :locked factory uses locked_at: 1.minute.ago to avoid stale-lock race at 300s boundary
spec/lib/postal/message_db/connection_pool_spec.rb: pool now yields Postal::MessageDB::Connection (not raw Mysql2::Client) — matchers updated
spec/lib/dns_resolver_spec.rb: excluded from CI (live DNS, flaky in GitHub Actions)
- CI runs the full rspec suite (806 examples) against MySQL adapter only; PG compatibility is validated at runtime
This issue tracks all divergences between this fork and postalserver/postal. It is kept as a living document.
Upstream base:
postalserver/postalmain atd038eaa(v3.3.7, 2026-06-03)Upstream PRs merged into this fork
make-userviaPOSTAL_INITIAL_USER_*env varsUpstream PRs evaluated but not merged
bundle updateon next releaseFork-specific features
Admin REST API v2
Full CRUD REST API mounted at
/api/v2/(JWT Bearer auth via Keycloak service account):GET/POST /api/v2/organizationsGET/DELETE /api/v2/organizations/:slugPOST/DELETE /api/v2/organizations/:slug/membersGET/POST /api/v2/usersGET /api/v2/users/:emailAuth: Keycloak service account (
client_credentials) issues JWT; Postal validatesazpclaim → super-admin access. Config (api.enabledis required — routes are only registered when it istrue):PostgreSQL support (
main_db+message_db)Both databases can be switched to PostgreSQL via config:
main_db: Rails multi-DB with RW primary + optional RO replica (connects_to)message_db: per-server PG schemas ("postal-server-{id}") instead of MySQL databasesProvisioner: dual MySQL/PG path; handles composite primary keys and prefix-length hints correctlydatabase.ymlrewritten: plain ERB interpolation, no YAML anchors/merge-keys (Psych 5 compat), no-%>(ERB 6/Prism compat)Multi-provider OIDC with optional PKCE (adapted from postalserver#3130)
Multiple OIDC providers configurable under
oidc.providers[](array). Each provider can have independent settings. PKCE is optional per provider:Single-provider flat config (
oidc.issuer,oidc.pkce) still supported for backwards compat.Keycloak OIDC + SCIM
postalclient, confidential)POST /auth/oidc/backchannel_logout— invalidates all active sessions for the userend_session_endpointon sign-outauto_provision_org: true— JIT org creation on first SSO loginclient_credentialsJWT for API v2 super-admin accessImage registry
CI publishes to
ghcr.io/startuppack/postal:latest(GitHub Container Registry, private org package) on every greenmain.Bug fixes (this fork, vs. vanilla Postal)
4b5bb7fconfig/initializers/omniauth.rb: addrequire "postal/oidc_providers"— file not eager-loaded at boot time causedNameError: uninitialized constant Postal::OIDCProvidersfe05672OidcProviders→OIDCProvidersacross 5 files — Railsinflect.acronym "OIDC"causes Zeitwerk to expectOIDCProviders, notOidcProviders1877937OidcLogoutController→OIDCLogoutControllerinoidc_logout_controller.rb— same Zeitwerk acronym inflection requirement9d2d721module Api→module APIin allapp/controllers/api/v2/files — Zeitwerk expects the constant to match the directory nameapi/→API2793959app/views/sessions/new.html.haml: renamePostal::OidcProviders→Postal::OIDCProviders— missed byfe05672, caused 500 on login pagef09c937spec/lib/dns_resolver_spec.rbfrom rspec — live DNS lookups againstdnstest.postalserver.ioare flaky in CI environmentsadeda20123ed60message_db/connection.rbinsert(): rescuePG::UndefinedColumnwhen appendingRETURNING id— themigrationstable hasversionas PK (notid); retry as plain INSERT93d9fa3message_db/provisioner.rbcreate_table_query_pg: prefix all PG index names with{table_name}_— PG index names are schema-scoped (unlike MySQL where they are table-scoped), causingPG::DuplicateTablewhen two tables share the same index name (e.g.on_message_id)b7e1c5amessage_db/provisioner.rb: add portableadd_column,add_index,modify_columnpublic methods; rewrite migrations 11–20 that used raw MySQL DDL (backtick quoting,USING BTREE,CONVERT TO CHARACTER SET utf8mb4) to use these helpers7716f73message_db/provisioner.rbPG_TYPE_MAP: addtinyint(no width) →smallintmapping — MySQL Mysql2 gem returnstinyint(1)as boolean but baretinyintas integer (0/1); PG must match. Migration 17parsedcolumn regressed totinyint(1)(→boolean), fixed back totinyint DEFAULT 0(→smallint)fd06996message_db/migrations/19_convert_database_to_utf8mb4.rb: guard withreturn if @database.provisioner.postgresql?— charset conversion (CONVERT TO CHARACTER SET utf8mb4) is a MySQL-only operation; PG is always UTF-8Known divergences from upstream (CI)
db:schema:loadinstead ofdb:migratein CI (bypasses Authie gem unversioned migration)spec/senders/smtp_sender_spec.rb: timeout context uses/timeout/regex (upstream sayserror)spec/lib/worker/jobs/process_queued_messages_job_spec.rb::lockedfactory useslocked_at: 1.minute.agoto avoid stale-lock race at 300s boundaryspec/lib/postal/message_db/connection_pool_spec.rb: pool now yieldsPostal::MessageDB::Connection(not rawMysql2::Client) — matchers updatedspec/lib/dns_resolver_spec.rb: excluded from CI (live DNS, flaky in GitHub Actions)