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

> Learn to implement push notifications with ntfy.sh in Property Web Builder. Discover a robust three-layer architecture for seamless, asynchronous delivery. Get the complete technical guide.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: how-to-guide
- Published: 2026-03-01

---

**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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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:

```ruby
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:

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

```

### Automating Listing Change Alerts

Add real-time publication notifications using ActiveRecord callbacks:

```ruby
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:

```ruby
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`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/website.rb)
- **Protocol compliance** is handled by `NtfyService` in [`app/services/ntfy_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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.