Implementing Push Notifications with ntfy.sh in Property Web Builder: A Complete Technical Guide

Property Web Builder implements push notifications with ntfy.sh through a tenant-aware, three-layer architecture that combines configuration persistence in ActiveRecord, a dedicated service class for HTTP orchestration, and background jobs for asynchronous delivery.

Property Web Builder is an open-source Rails platform designed for multi-tenant real estate websites. The application natively integrates push notifications with ntfy.sh to alert administrators about inquiries, listing changes, and security events without requiring external SaaS providers or complex mobile SDKs.

Architecture Overview

The notification system follows a strict separation of concerns across three layers. The Pwb::Website model stores tenant-specific configuration and feature flags. The NtfyService class handles topic construction, HTTP header assembly, and error logging. Finally, NtfyNotificationJob decouples network latency from the web request cycle using ActiveJob.

This design ensures that each tenant operates in complete isolation. Topic prefixes prevent notification leakage between websites sharing the same ntfy.sh server instance.

Configuring the Pwb::Website Model

In app/models/pwb/website.rb (lines 49-56), the website model persists all ntfy configuration as standard ActiveRecord attributes. The schema migration at db/migrate/20251208134208_add_ntfy_settings_to_websites.rb (lines 4-13) establishes the required boolean and string columns.

Key configuration attributes include:

  • ntfy_server_url: The ntfy endpoint (defaults to https://ntfy.sh for public instances or custom URLs for self-hosted deployments)
  • ntfy_topic_prefix: A string prepended to all topics for tenant isolation
  • ntfy_access_token: Optional bearer token for publishing to private ntfy topics
  • ntfy_notify_inquiries, ntfy_notify_listings: Boolean flags gating specific notification channels

The model provides the enabled_for?(channel) method, which NtfyService consults before dispatching any payload.

The NtfyService Implementation

Located in app/services/ntfy_service.rb (lines 17-62), this service class encapsulates the ntfy protocol implementation. It transforms internal domain events into properly formatted HTTP POST requests with the correct header schema required by ntfy.sh.

HTTP Publishing Mechanics

The private publish method constructs the topic string using the pattern <prefix>-<channel>. It assembles mandatory HTTP headers including Title, Priority, Tags, Click, and Actions, then executes the POST request to the configured server URL. Errors and timeouts are captured and logged via StructuredLogger to prevent job failures from losing notification state.

Notification Type Methods

The service exposes specific methods for each business event:

  • test_configuration(website) (lines 64-84): Validates connectivity by sending a test payload to the configured server
  • notify_inquiry(website, message) (lines 35-51): Formats contact form submissions with inquiry metadata
  • notify_listing_change(website, listing, change_type) (lines 53-71): Handles property publication status changes, utilizing the listing_change_content helper (lines 309-352) for body formatting
  • notify_security_event(website, event_type, metadata) (lines 73-76): Dispatches authentication and access anomaly alerts

Each method respects the tenant's feature flags before invoking publish.

Asynchronous Delivery with NtfyNotificationJob

The app/jobs/ntfy_notification_job.rb file (lines 19-52) defines a TenantAwareJob that switches database context to the correct website before executing. This prevents cross-tenant data leakage in multi-threaded job processors.

The job accepts parameters for the website ID, notification type, record ID, and optional metadata. It retrieves the relevant ActiveRecord object (such as Pwb::Message or Pwb::SaleListing) and delegates to the appropriate NtfyService method. By queuing the HTTP POST operation asynchronously, the application maintains sub-100ms response times on web endpoints regardless of ntfy.sh latency.

Typical execution flow:

  1. User submits contact form → Rails controller creates Pwb::Message record
  2. Model callback triggers NtfyNotificationJob.perform_later(website.id, :inquiry, message.id)
  3. Job processor switches tenant context and loads the message
  4. Job calls NtfyService.notify_inquiry(website, message)
  5. Service POSTs to https://ntfy.sh/<topic> with structured headers

Implementation Examples

Testing Connectivity from the Rails Console

Verify integration settings before enabling notifications in production:

website = Pwb::Website.find(1)
result = NtfyService.test_configuration(website)
puts result[:message]   # => "Test notification sent successfully"

Triggering Inquiry Notifications Manually

While typically invoked via callbacks, you can manually dispatch inquiry alerts:

message = Pwb::Message.find(42)
NtfyService.notify_inquiry(message.website, message)

Automating Listing Change Alerts

Add real-time publication notifications using ActiveRecord callbacks:

class Pwb::SaleListing < ApplicationRecord
  after_update :notify_if_published

  private

  def notify_if_published
    return unless saved_change_to_attribute?(:published) && published?
    NtfyService.notify_listing_change(website, self, :published)
  end
end

Enqueueing Security Events

For non-persisted events like failed logins, pass raw metadata to the job:

NtfyNotificationJob.perform_later(
  website.id,
  :security,
  nil,
  nil,
  'login_failed',
  { email: user.email, ip: request.remote_ip }
)

The job routes this to NtfyService.notify_security_event for immediate dispatch.

Summary

  • Tenant isolation is enforced through topic prefixes stored in ntfy_topic_prefix on the Pwb::Website model
  • Feature gating relies on boolean columns (ntfy_notify_inquiries, ntfy_notify_listings) checked via enabled_for? in app/models/pwb/website.rb
  • Protocol compliance is handled by NtfyService in app/services/ntfy_service.rb, which constructs proper ntfy HTTP headers and manages error logging
  • Async delivery is guaranteed by NtfyNotificationJob in app/jobs/ntfy_notification_job.rb, preventing request-cycle blocking
  • Extensibility requires only adding methods to NtfyService, corresponding migration columns, and job routing logic

Frequently Asked Questions

How do I enable push notifications for a specific property website?

Set the ntfy_server_url (defaulting to https://ntfy.sh), define a unique ntfy_topic_prefix for isolation, and toggle the relevant boolean columns such as ntfy_notify_inquiries to true on the Pwb::Website record. The NtfyService automatically validates these flags via enabled_for? before transmitting any payload.

Can Property Web Builder use a private ntfy instance instead of ntfy.sh?

Yes. Configure the ntfy_server_url attribute on the website model to point to your self-hosted ntfy server. If the server requires authentication, populate the ntfy_access_token column; the service includes this as a Bearer token in the Authorization header of all POST requests.

What happens when the ntfy server is unreachable?

The publish method in app/services/ntfy_service.rb wraps HTTP operations in exception handling that logs failures through StructuredLogger. Because NtfyNotificationJob processes delivery asynchronously, ActiveJob's built-in retry mechanisms automatically reattempt failed transmissions according to your queue adapter configuration.

How do I add a custom notification channel for new business events?

Extend app/services/ntfy_service.rb with a new method following the pattern of notify_inquiry, define a channel constant for the topic suffix, add a corresponding boolean enablement column to pwb_websites via a Rails migration, and update NtfyNotificationJob to route the new event type. The existing infrastructure handles topic construction, tenant context switching, and HTTP delivery automatically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →