Appearance
Store-only iOS alerts
Authenticated GET /me and POST /ios/push/devices return private_ios_push: true to advertise this additive API, and push_enabled to report whether APNs is configured. They do not confirm iPhone receipt.
Send a private completion alert through the existing topic API:
json
{"text":"Durable result", "tags":["finished"], "store_only":true, "notify_ios":true}notify_ios requires store_only, a non-direct valid topic and an explicit finished, needs_input or reminder tag. It rejects chat IDs, thread IDs and reply controls. Anonymous publishers cannot opt in, including through batches or reminder creation. Publish authorization and topic device registration scopes are unchanged. Existing store-only requests without the new flag never alert.
Hooka stores the message and notifies SSE before attempting regular APNs to registered topic devices. It skips route lookup, default chats, all external providers and VoIP. No configured APNs sender or registered device causes no fallback. Notification text is generic and fixed by the registered app bundle; request/model/reminder content is never copied into these APNs alerts. The navigation payload carries the existing topic/message references. Completion alerts respect per-device foreground presence. Reminders still alert in app.
POST /reminders accepts the same two flags inside notification, with the usual schedule fields and reminder tag. Flags survive persistence, recurring fires and retries. Create/get/list responses show delivery:{mode:"ios_push",resolution:"best_effort",topic:"..."}. Private reminder create/cancel audit events are stored without administrative provider fanout; legacy reminder audit behavior is unchanged.
Use an immutable request and stable Idempotency-Key (maximum 200 bytes) for reminder create retries. The key is scoped to the authenticated creator and returns the original reminder even when the replayed date is now past. Reusing a key with changed content also returns the original; clients must verify the receipt. A new intent requires a new key.
APNs remains best effort and asynchronous. Publish receipts, zero delivery counts and reminder fired do not prove provider or device acceptance. History is the recovery source. Normal publish idempotency prevents duplicate attempts, but a crash between persistence and APNs can omit an alert; this contract does not introduce a push outbox. Clients should not mirror these server reminders locally or they can show duplicate alerts. Older servers ignore notify_ios while honoring store_only; clients must require the positive capability before enabling reminder creation.
Digest and classifier exclusion
For silent private storage, authenticated publishers can explicitly set store_only:true, exclude_from_digest:true. This requires a valid non-direct topic without external recipient/thread/reply controls; it does not send APNs. The flag also applies inside reminder notification. Private image uploads always exclude their marker rows, even without a client flag.
Private APNs, explicit private storage and private lifecycle audit rows are inserted atomically with server-owned messages.exclude_from_digest=true. Topic digest cursors omit these rows, including when a digest is enabled later. Neither a caller's false payload field nor a replay can turn a private row into digest input. Ordinary store_only publications remain eligible because existing digest producers use that mode intentionally.
GET /search?exclude_private=true applies the same policy before returning input to a classifier or digest; the response confirms it in applied.exclude_private:true. Normal authenticated history/search is unchanged. The internal client and Go SDK's SearchOptions.ExcludePrivate require that exact boolean acknowledgment, failing closed on an older or incompatible server. The built-in hooka-digest tag and run commands request it before model I/O.
Deployment runs the additive, idempotent database migration before serving this API. It adds the boolean column (default false) and backfills only rows whose stored payload explicitly has store_only:true plus either notify_ios:true or exclude_from_digest:true, and rows with server-owned source=private_image. Private audit records use internal policy even with an empty payload.
Once private rows exist, do not roll back to a server or digest CLI version that ignores this exclusion while digest jobs are running. Stop digest jobs first and keep the additive schema; a schema-compatible rollback is not automatically privacy-compatible. No database rollback should clear the exclusion column.
Upgrading a deployment with existing rows
This API does not infer private intent from older store_only messages. Rows written before a client adopted notify_ios or exclude_from_digest stay eligible for digests until they are explicitly marked or replaced by writers that send the new flag. A backfill should be scoped to exact, verified topics; do not guess every namespace or every store-only row.
Before upgrading, check which scheduled classifier/digest jobs run against which topics. Keep broad jobs paused until the server migration and the exclusion-aware hooka-digest are deployed, writers have adopted the flag, and any historical rows you want excluded are marked. Older digest CLIs ignore the policy because their ordinary searches remain authorized, so a server upgrade alone is not enough. The upgrade itself performs no backfill.