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.
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.
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.
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"
}'
| Field | Use it for | Default |
|---|---|---|
title | The required, human-readable summary | — |
body | Supporting detail, up to 1,000 characters | Empty |
channel | Subscription and credential routing | default |
url | An HTTPS destination opened from the event | Empty |
type | A machine-readable event name | Empty |
severity | Visual meaning from debug through critical | info |
data | A small JSON object with structured context | {} |
dedupe_key | Safe retry identity for one publisher | Empty |
ttl_seconds | Time before a transient event becomes stale | No expiry |
correlation_id | Connect related signals and answers | Empty |
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.
Choose the lane.
Use stable lowercase names such as deploys, alerts, or home.front-door.
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.
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.
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.
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.
| Mode | What Firebase carries | What Pushify stores |
|---|---|---|
direct_fcm | The ordinary notification payload | Readable event content |
opaque_fcm | A generic wake-up | Readable event content |
e2ee_required | A generic wake-up | Per-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.
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
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.
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.
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.
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"
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.
| Credential | Intended capability |
|---|---|
| Automation publisher | Publish only to granted channel IDs and event types; read answers to its own events |
| Automation listener | Read only from granted channel IDs and event types |
| Android installation | Identify one enrolled phone and use only its exact receive, publish, and response grants |
| Administrator | Manage 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.
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())
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.
| Status | Meaning | What to do |
|---|---|---|
400 | Invalid JSON, field, action, or response value | Fix the request; do not retry unchanged |
401 | Missing or invalid credential | Check the secret source without logging the token |
403 | Valid credential outside its permission or scope | Use the intended scoped credential |
409 | An interactive event is already closed | Treat the action as no longer available |
410 | The event or action has expired | Request a fresh interaction if still needed |
413 | The request body exceeds 16 KiB | Reduce it; Pushify never publishes a truncated body |
415 | The shorthand content type is unsupported | Use UTF-8 text or application/json |
429 | Rate or stream-concurrency limit reached | Respect 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 →