pushify.me
Private beta

MANUAL / 0.1 PRIVATE-BETA REFERENCE

Send a signal.
Keep the context.

This manual documents the current private beta. It is public for review, but Pushify credentials and the Android app are not publicly available yet.

Preview documentation Examples require a scoped credential. Interfaces may still change before release.

01 / MENTAL MODEL

How Pushify works

A publisher sends one authenticated JSON event. Pushify validates and stores it before delivery, then makes the same event available to subscribed Android phones, retained history, and live listeners.

1PublishScript, service, bot, or phone
2PersistOne canonical, durable event
3DeliverAndroid, history, or listener

Interactive events complete the loop: a phone can choose an action or fill in a small native form, and the originating publisher can poll or stream the authenticated answer.

02 / FIRST SIGNAL

Only the title is required.

Use an individually revocable publisher credential with access to the target channel. Keep it in an environment variable and send it in the authorization header. Never put it in the URL or browser code. The channel in the path is routing, not a password.

BASHSend a basic notification
export PUSHIFY_URL="https://pushify.me"

curl --fail-with-body "$PUSHIFY_URL/backups" \
  -H "Authorization: Bearer $PUSHIFY_PUBLISHER_CREDENTIAL" \
  --data 'Backup complete'

A new event returns HTTP 202. Curl's default form content type is treated as literal text here. The response includes its generated ID, sequence, timestamps, payload, and delivery counts. Use the versioned JSON route in the next section for structured fields.

03 / STRUCTURED EVENTS

Add context machines can use.

The title and body are for people. Fields such as type, data, and correlation_id let listeners route and connect events without parsing prose.

JSONA complete deployment signal
curl --fail-with-body "$PUSHIFY_URL/api/v1/push" \
  -H "Authorization: Bearer $PUSHIFY_PUBLISHER_CREDENTIAL" \
  -H "Content-Type: application/json" \
  --data '{
    "title": "Production deploy completed",
    "body": "Version 2.8.1 is healthy.",
    "channel": "deploys",
    "url": "https://example.com/runs/1842",
    "type": "deploy.completed",
    "severity": "success",
    "data": {"version":"2.8.1","sha":"abc123"},
    "dedupe_key": "production-abc123",
    "ttl_seconds": 3600,
    "correlation_id": "release-1842"
  }'
FieldUse it forDefault
titleThe required, human-readable summary
bodySupporting detail, up to 1,000 charactersEmpty
channelSubscription and credential routingdefault
urlAn HTTPS destination opened from the eventEmpty
typeA machine-readable event nameEmpty
severityVisual meaning from debug through criticalinfo
dataA small JSON object with structured context{}
dedupe_keySafe retry identity for one publisherEmpty
ttl_secondsTime before a transient event becomes staleNo expiry
correlation_idConnect related signals and answersEmpty

04 / ROUTING

Separate routing from urgency.

Channels decide who receives or consumes an event. Severity describes its meaning. A phone subscribed to deploys will not receive an event sent only to backups. Channel names are visible routing labels, never access credentials.

CHANNELS

Choose the lane.

Use stable lowercase names such as deploys, alerts, or home.front-door.

SEVERITY

Describe the state.

Choose debug, info, success, warning, error, or critical.

Private-beta administrators manage channels as catalog entries with a permanent ID, stable slug, display name, description, and active or archived state. Android shows the catalog as a selectable list and keeps a cached copy for offline settings. Archiving blocks new sends and subscriptions without deleting retained history.

A catalog name grants nothing.

Administrators create every channel explicitly. Database-backed principals receive exact channel-ID, operation, and event-type grants; unknown slugs are denied. Private groups add enforced owner, manager, publisher, and subscriber membership without treating a name or passphrase as permanent authority.

Manage private groups and your account on Android.

The Groups screen can create or join private channels, review invitations before redemption, manage roles and ownership, revoke invitations, block or report another member, and issue a remote channel purge. A block prevents new invitation redemption in either direction; it does not silently eject either person from a shared group.

The Account screen lists only your devices, scoped API tokens, quota usage, blocks, reports, and support cases. It can save a metadata export that excludes credentials, FCM tokens, and messages. Account closure requires the exact confirmation and is blocked while you own an active group.

05 / SCOPED ACCESS

Every device gets its own authority.

Android enrollment uses a short-lived, one-time token. The resulting installation session is individually revocable and carries exact channel and capability grants.

No shared group password.

Membership, installation state, and scoped grants decide what each device can publish, receive, or answer.

Removing a member, revoking an installation, suspending an account, or changing grants invalidates the affected authority without disturbing unrelated devices.

06 / SENSITIVE EVENTS

Ask the phone to keep less.

A sensitive event uses a generic lock-screen notification and requires a supported Android client. You can give it an absolute deletion deadline, start a timer when the recipient first opens it, or use both.

JSONDelete five minutes after opening
curl --fail-with-body "$PUSHIFY_URL/api/v1/push" \
  -H "Authorization: Bearer $PUSHIFY_PUBLISHER_CREDENTIAL" \
  -H "Content-Type: application/json" \
  --data '{
    "title": "Temporary recovery detail",
    "body": "Read this on the enrolled phone.",
    "channel": "incident-room",
    "severity": "critical",
    "sensitive": true,
    "delete_after_open_seconds": 300,
    "ttl_seconds": 3600
  }'

Remote channel purge and whole-app wipe are authenticated commands with delivery status and anti-resurrection tombstones. Two-person mode requires a different trusted principal and a separate signed approval. These controls reduce retained copies, but they cannot erase screenshots, camera photos, malicious client changes, or a device that never reconnects.

07 / ENCRYPTED CHANNELS

Encryption is a channel decision.

Ordinary channels use authenticated HTTPS and readable server-side events. An administrator can prepare a private channel for end-to-end encryption, check that every member has a compatible crypto device, and then activate e2ee_required. Pushify will not silently fall back to plaintext after activation.

ModeWhat Firebase carriesWhat Pushify stores
direct_fcmThe ordinary notification payloadReadable event content
opaque_fcmA generic wake-upReadable event content
e2ee_requiredA generic wake-upPer-device libsignal ciphertext and routing metadata

The stateful automation client replaces curl for an encrypted channel because it owns the publisher's libsignal identity and retry journal. Its bearer, state database, and separate wrapping key belong in protected storage.

CLIEnroll and publish encrypted content
export PUSHIFY_SERVER="https://pushify.me"
export PUSHIFY_STATE_DIR="/var/lib/pushify-e2ee/incident-room"

pushify-e2ee enroll

jq -n --arg title "Encrypted test" \
  '{title:$title,channel:"incident-room",severity:"critical"}' |
  pushify-e2ee publish 0123456789abcdef0123456789abcdef
Private-beta security boundary

Official libsignal code handles pairwise encryption. The server still controls the device directory and traffic metadata. Verified-roster enforcement, hardware-required endpoints, independent review, licensing, and public package distribution remain closed release gates.

08 / RELIABILITY

Retries should not become noise.

Give repeat attempts the same dedupe_key. The first request creates and delivers the event. A retry by the same credential returns the original event with "duplicate": true and does not send it again.

Use ttl_seconds when a late event would be misleading. The allowed range is 60 seconds through 28 days. Expired events disappear from history and replay. Current Android clients also enforce supported local expiry, delete-after-open, and acknowledged purge commands, but no lifecycle control can defeat screenshots, malicious clients, or every offline copy.

09 / INTERACTION

Let the phone answer.

An event can carry up to eight actions. A safe quick action may appear directly on the notification. Destructive actions require confirmation. Actions can also collect choices, booleans, numbers, or short text through native Android controls.

JSONAsk for a safe diagnostic action
curl --fail-with-body "$PUSHIFY_URL/api/v1/push" \
  -H "Authorization: Bearer $PUSHIFY_PUBLISHER_CREDENTIAL" \
  -H "Content-Type: application/json" \
  --data '{
    "title": "Disk space is filling up",
    "channel": "alerts",
    "type": "server.disk_warning",
    "severity": "warning",
    "action_ttl_seconds": 3600,
    "actions": [
      {
        "id": "disk_info",
        "label": "Get disk info",
        "style": "positive",
        "quick": true,
        "closes": false
      },
      {
        "id": "ignore",
        "label": "Ignore",
        "style": "destructive",
        "confirmation": "Ignore this warning?"
      }
    ]
  }'

10 / RESPONSES

Continue from the answer.

The immutable automation principal that published an event can read answers only for its own events. Poll from a saved response cursor, process the complete page, and then store next_after.

BASHPoll phone answers
curl --fail-with-body \
  "$PUSHIFY_URL/api/v1/responses?after=0&limit=100" \
  -H "Authorization: Bearer $PUSHIFY_PUBLISHER_CREDENTIAL"

Answers include the original event, selected action_id, validated field values, device identity, correlation ID, and their own increasing sequence.

11 / HISTORY AND STREAMS

Catch up, then stay connected.

Event history and Server-Sent Events use the same durable sequence cursor. Without a cursor, a stream begins at the current end. With after, it replays retained events and then follows new ones. Events and responses are retained for 90 days.

SSEReplay and follow two channels
curl --no-buffer --fail-with-body \
  "$PUSHIFY_URL/api/v1/stream?channels=deploys,alerts&after=42" \
  -H "Authorization: Bearer $PUSHIFY_LISTENER_CREDENTIAL" \
  -H "Accept: text/event-stream"
Reliable handlers

The bundled Linux listener saves its cursor only after the configured handler succeeds, reconnects with backoff, and never evaluates event content as shell code. Public installation material will accompany a later release.

12 / CREDENTIALS

Give every service one lane.

Bearer tokens belong in server-side environment variables or protected configuration. Do not embed them in URLs, frontend JavaScript, screenshots, or logs.

CredentialIntended capability
Automation publisherPublish only to granted channel IDs and event types; read answers to its own events
Automation listenerRead only from granted channel IDs and event types
Android installationIdentify one enrolled phone and use only its exact receive, publish, and response grants
AdministratorManage users, channels, installations, principals, grants, revocation, and safe audit metadata

13 / RECIPES

Use ordinary HTTP.

These server-side examples all read the token from PUSHIFY_PUBLISHER_CREDENTIAL. They deliberately use standard libraries and send the same title-only payload.

PYTHONStandard library
import os, urllib.request

request = urllib.request.Request(
    "https://pushify.me/jobs",
    data="Job complete".encode(),
    headers={
        "Authorization": f"Bearer {os.environ['PUSHIFY_PUBLISHER_CREDENTIAL']}",
        "Content-Type": "text/plain; charset=utf-8",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    print(response.read().decode())
JAVASCRIPTServer-side fetch
const response = await fetch("https://pushify.me/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PUSHIFY_PUBLISHER_CREDENTIAL}`,
    "Content-Type": "text/plain; charset=utf-8",
  },
  body: "Job complete",
});

if (!response.ok) throw new Error(`Pushify returned ${response.status}`);
console.log(await response.json());

14 / ERRORS

Fail clearly and retry carefully.

StatusMeaningWhat to do
400Invalid JSON, field, action, or response valueFix the request; do not retry unchanged
401Missing or invalid credentialCheck the secret source without logging the token
403Valid credential outside its permission or scopeUse the intended scoped credential
409An interactive event is already closedTreat the action as no longer available
410The event or action has expiredRequest a fresh interaction if still needed
413The request body exceeds 16 KiBReduce it; Pushify never publishes a truncated body
415The shorthand content type is unsupportedUse UTF-8 text or application/json
429Rate or stream-concurrency limit reachedRespect Retry-After and back off

Publish requests are limited per credential and client address during the beta. Use a stable deduplication key before retrying a request whose outcome is uncertain.

END OF CURRENT MANUAL

Still private. Getting clearer.

This reference will be reviewed again after the current stability test. Public onboarding and Android installation instructions will appear only when access opens.

Follow private-beta progress