> ## Documentation Index
> Fetch the complete documentation index at: https://www.towbar.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# ntfy

> ntfy sends and receives notifications through topics you can subscribe to.

export const UseCaseNavigation = () => {
  useLayoutEffect(() => {
    const storageKey = "towbar-use-case-open-groups";
    const groupSelector = "#sidebar li[data-title] > button[aria-expanded]";
    let restoring = false;
    let frame = 0;
    function storedGroups() {
      try {
        return new Set(JSON.parse(sessionStorage.getItem(storageKey) || "[]"));
      } catch {
        return new Set();
      }
    }
    function save(groups) {
      try {
        sessionStorage.setItem(storageKey, JSON.stringify([...groups]));
      } catch {}
    }
    function groupName(button) {
      return button.parentElement?.dataset.title;
    }
    function rememberOpenGroups() {
      const groups = new Set();
      for (const button of document.querySelectorAll(groupSelector)) {
        if (button.getAttribute("aria-expanded") === "true") {
          const name = groupName(button);
          if (name) groups.add(name);
        }
      }
      save(groups);
    }
    function restoreOpenGroups() {
      frame = 0;
      const groups = storedGroups();
      if (!groups.size) return;
      restoring = true;
      for (const button of document.querySelectorAll(groupSelector)) {
        if (groups.has(groupName(button)) && button.getAttribute("aria-expanded") === "false") {
          button.click();
        }
      }
      restoring = false;
    }
    function scheduleRestore() {
      if (!frame) frame = requestAnimationFrame(restoreOpenGroups);
    }
    function onClick(event) {
      if (restoring || !(event.target instanceof Element)) return;
      const button = event.target.closest(groupSelector);
      if (button) {
        const name = groupName(button);
        if (!name) return;
        const groups = storedGroups();
        if (button.getAttribute("aria-expanded") === "true") groups.delete(name); else groups.add(name);
        save(groups);
        return;
      }
      if (event.target.closest('a[href^="/docs/use-cases/"]')) {
        rememberOpenGroups();
      }
    }
    const observer = new MutationObserver(scheduleRestore);
    document.addEventListener("click", onClick, true);
    observer.observe(document.getElementById("sidebar") || document.body, {
      childList: true,
      subtree: true
    });
    scheduleRestore();
    return () => {
      document.removeEventListener("click", onClick, true);
      observer.disconnect();
      cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<UseCaseNavigation />

<img className="towbar-doc-brand-logo" src="https://mintcdn.com/avgeek/EGkEWLTQnOwecqNx/assets/use-case-logos/ntfy.svg?fit=max&auto=format&n=EGkEWLTQnOwecqNx&q=85&s=335f408f9678c778333387fa87969cab" alt="ntfy logo" aria-hidden="true" width="24" height="24" data-path="assets/use-case-logos/ntfy.svg" />

**Upstream source:** [Current installation guide](https://docs.ntfy.sh/install/#docker).

ntfy needs a `serve` command and an explicit access policy. This Compose project stores its message cache and user database in one named volume; it needs no SQL Datastore.

## Towbar manifest

```yaml title=".towbar/services/ntfy.compose.yml" theme={"system"}
id: ntfy
name: ntfy
file: deploy/ntfy/compose.yml
strategy: maintenance
services:
  ntfy:
    port: 80
    domains:
      - ntfy.example.com
    ingress:
      type: proxy
environments:
  production:
    server: 192.0.2.10
```

## Compose project

<CodeGroup>
  ```yaml title="deploy/ntfy/compose.yml" theme={"system"}
  services:
    ntfy:
      image: binwiederhier/ntfy:v2.15.0@sha256:aa10e84da624f65be107f9317dbf6e212fa812e0ebf62e74d032d0762eccc930
      command:
        - serve
      configs:
        - source: server
          target: /etc/ntfy/server.yml
      volumes:
        - data:/var/cache/ntfy
  configs:
    server:
      file: ./server.yml
  volumes:
    data: {}
  ```

  ```yaml title="deploy/ntfy/server.yml" theme={"system"}
  base-url: https://ntfy.example.com
  cache-file: /var/cache/ntfy/cache.db
  auth-file: /var/cache/ntfy/user.db
  auth-default-access: deny-all
  ```
</CodeGroup>

## Configure

1. Prepare the example server, connect the repository, and map `production` to the branch containing these files. Replace the example server IP and domain.
2. Commit every file shown below under `deploy/ntfy/`. The Towbar manifest points to the Compose file; it does not create it for you.
3. Sync the repository and inspect the resolved Compose project. Save the runtime values below if this example declares any.
4. Create an administrator with ntfy's `user add --role=admin` command in the running container before publishing private topics. `deny-all` blocks anonymous reads and writes by default.
5. Back up the `data` volume if cached notifications or account records must survive server loss.
6. Deploy it manually and run the verification below before enabling auto-deploy.

## Verify

Publish and subscribe to a test topic with the administrator credentials. Verify that an unauthenticated request is denied.

For field constraints, see [Compose guide](/docs/services/modes/compose).
