Notifications & API Endpoints

Three separate things. Push notifications reach the users of the app you built. Ocalt Notifications reach you, the namespace owner, when your own automation has something to say. API endpoints are ordinary Site Mode scripts that answer requests instead of serving pages.

Implementation pending. The verbs on this page are designed and specified below. Runtime implementation is in progress.

How Push Works on Your Subdomain

Registering a subdomain does more than create a folder. The service worker and every resource push needs are copied into that subdomain’s folder at registration time, automatically. They are served from your own origin, as your own files, from the moment the subdomain exists.

That is the whole mechanism. When a script calls a push verb, it works against the service worker already sitting in the folder — nothing to install, no snippet to paste into your pages, no consent portal on another domain to redirect through. The browser is being asked for permission by your site, because the service worker is genuinely served by your site.

The files are yours. They live in your subdomain folder alongside everything else you put there, and count against your namespace exactly as any other file does. Leave them where they are and push works; the Site Mode router serves them like any other static file.
?uid is your own end-user identifier, exactly as used by the Accounts System. It has nothing to do with an Ocalt platform login — these are the users of the app you built.

Checking and Requesting Push Access

Check Subscription Status
NOTIFY CHECK PUSH STATUS FOR ?uid SET ?has_push
AFTER EMIT ?has_push
Request Push Permission
NOTIFY REQUEST PUSH GRANT FOR ?uid
(* The browser prompts on your own origin, through the service worker already
   in the subdomain folder. Nothing is redirected anywhere. *)

Recommended Flow

Check, Then Request If Needed
ACCOUNT SESSION ?token SET ?user
AFTER NOTIFY CHECK PUSH STATUS FOR ?user("id") SET ?has_push
AFTER IF ?has_push IS EQUAL TO false
OPEN
  NOTIFY REQUEST PUSH GRANT FOR ?user("id")
CLOSE

PUSH SUBSCRIBE / UNSUBSCRIBE — Manual Registration

For advanced use, if you handle the browser subscription yourself and want to register it directly. ?sub is the real browser PushSubscription object — {"endpoint": "...", "keys": {"p256dh": "...", "auth": "..."}}. Multiple subscriptions per ?uid are additive, one per device, not replacing each other.

Registering a Subscription Directly
PARSE JSON !REQUEST('body') SET ?sub
AFTER PUSH SUBSCRIBE ?sub AS ?user("id") SET ?ok
AFTER EMIT ?ok
Removing Every Subscription for a User
PUSH UNSUBSCRIBE ?user("id") SET ?n
AFTER EMIT "Removed " & ?n & " subscriptions"
(* Removes every device's subscription for this user, not just one *)

NOTIFY — Sending a Notification

Delivered to every active subscription for ?uid, across every device.

Full Notification
NOTIFY "Your order has shipped!" TO ?uid
WITH "Order Update"
ICON "https://myapp.ocalt.site/icon.png"
IMAGE "https://myapp.ocalt.site/order-photo.jpg"
LINK "https://myapp.ocalt.site/orders/42"
ACTIONS ["View Order" AND "Track Package"]
SET ?id
Minimal Notification — Title and Body Only
NOTIFY "Your order has shipped!" TO ?uid WITH "Order Update" SET ?id

Notification History

Listing, Marking Read, and Clearing
NOTIFY LIST FOR ?uid SET ?notifications
AFTER NOTIFY MARK READ ?id
AFTER NOTIFY CLEAR ?id
AFTER NOTIFY CLEAR ALL FOR ?uid

Ocalt Notifications — Telling Yourself

Everything above sends to your users. NOTIFICATION sends to you — the owner of the namespace the script is running in. It takes no recipient, because there is only one: whoever triggered it.

This is what a watch, a cron job or a schedule uses to report. Those run with no one watching, long after the script that registered them is gone, so a plain EMIT goes nowhere. NOTIFICATION reaches you wherever you are.

A Job Reporting In
NOTIFICATION "Job done"
From Inside a Watch Trigger
NEW WATCH [COUNT ROWS FROM DB "shopdb" TABLE "orders"] EVERY 1 MINUTE ONCHANGE [SIDELOAD `
COUNT ROWS FROM DB "shopdb" TABLE "orders" SET ?n
AFTER NOTIFICATION "Order count is now " & ?n
`] SET ?watch
Reporting a Failure From a Cron Job
FETCH "https://api.partner.com/sync" SET ?data
OR CATCH ERROR
OPEN
  NOTIFICATION "Nightly sync failed: " & !ERROR('message')
CLOSE
Not the same as push. NOTIFY ... TO ?uid reaches a user of your app, through the service worker on your subdomain. NOTIFICATION "..." reaches the Ocalt account that owns the namespace, delivered through notifications.ocalt.com. They share the underlying push transport and nothing else — different audience, different origin, different verb.

Your Own API Endpoint

An API endpoint is not a special kind of object in OcaltQL. It is an ordinary .oql file in your site folder that answers a request instead of drawing a page. Because the Site Mode router falls back to .oql for an extensionless path, api.oql is reachable at /api with no routing configuration at all.

/root/sites/mysite/api.oql — reachable at https://mysite.ocalt.site/api
HEADER "Content-Type" AS "application/json"
AFTER IF !REQUEST('method') IS IDENTICAL TO "POST"
OPEN
  PARSE !REQUEST('body') AS JSON SET ?in
  AFTER INSERT INTO DB "shopdb" TABLE "orders" ROW "item" AS ?in("item") SET ?id
  AFTER STATUS 201
  AFTER EMIT `{"id":` & ?id & `}`
CLOSE
OR
OPEN
  STATUS 405
  AFTER EMIT `{"error":"method not allowed"}`
CLOSE

Nothing here is new. !REQUEST, !GET and !POST read the incoming request; HEADER and STATUS shape the response; EMIT is the body. The same verbs that build a page build an endpoint.

A Whole REST Resource in One File
HEADER "Content-Type" AS "application/json"
AFTER SWITCH !REQUEST('method')
OPEN
  CASE "GET"
    SELECT ROWS FROM DB "shopdb" TABLE "orders" WHERE "id" IS EQUAL TO !GET('id') SET ?rows
    AFTER CAST ?rows AS JSON SET ?out
    AFTER EMIT ?out
    BREAK

  CASE "POST"
    PARSE !REQUEST('body') AS JSON SET ?in
    AFTER INSERT INTO DB "shopdb" TABLE "orders" ROW "item" AS ?in("item") SET ?id
    AFTER STATUS 201
    AFTER EMIT `{"id":` & ?id & `}`
    BREAK

  CASE "DELETE"
    DELETE ROW FROM DB "shopdb" TABLE "orders" WHERE "id" IS EQUAL TO !GET('id')
    AFTER STATUS 204
    BREAK

  DEFAULT
    STATUS 405
    AFTER EMIT `{"error":"method not allowed"}`
    BREAK
CLOSE
Receiving a Signed Event From an External Service
(* /root/sites/mysite/hooks.oql — reachable at https://mysite.ocalt.site/hooks *)
GENERATE HASH FOR !REQUEST('body') AS hmac-sha256 KEY "shared_secret_key" SET ?expected
AFTER ACCOUNT COMPARE ?expected WITH !REQUEST('header:X-Signature') SET ?valid
AFTER IF ?valid IS EQUAL TO false
OPEN
  STATUS 401
  AFTER EMIT `{"error":"bad signature"}`
CLOSE
OR
OPEN
  PARSE !REQUEST('body') AS JSON SET ?event
  AFTER NOTIFICATION "Webhook received: " & ?event("type")
  AFTER STATUS 200
CLOSE
Authentication is yours to choose. A Site Mode endpoint runs as the site owner with no credentials in the request, so anything reaching /api is anonymous until your script says otherwise. Use ACCOUNT SESSION for your own users, ACCOUNT COMPARE against a shared secret for machine callers — it is constant-time, which a plain IS IDENTICAL TO is not.

WEBHOOK SEND — Signed Outbound Events

SECRET signs the payload with an HMAC header, so the receiving system can verify the request genuinely came from your namespace and was not tampered with in transit.

Sending a Signed Event
NEW OBJECT SET ?payload
AFTER SET ?payload("event") AS "payment.completed"
AFTER SET ?payload("user_id") AS ?uid
AFTER SET ?payload("amount") AS ?amount
AFTER SET ?payload("timestamp") AS !NOW('timestamp')
AFTER WEBHOOK SEND ?payload TO "https://partner.example.com/hooks" SECRET "shared_secret_key" SET ?response
AFTER IF ?response("status") IS EQUAL TO 200
OPEN
  EMIT "Delivered"
CLOSE
OR
OPEN
  EMIT "Failed"
CLOSE