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.
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.
?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
NOTIFY CHECK PUSH STATUS FOR ?uid SET ?has_push
AFTER EMIT ?has_push
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
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.
PARSE JSON !REQUEST('body') SET ?sub
AFTER PUSH SUBSCRIBE ?sub AS ?user("id") SET ?ok
AFTER EMIT ?ok
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.
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
NOTIFY "Your order has shipped!" TO ?uid WITH "Order Update" SET ?id
Notification History
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.
NOTIFICATION "Job done"
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
FETCH "https://api.partner.com/sync" SET ?data
OR CATCH ERROR
OPEN
NOTIFICATION "Nightly sync failed: " & !ERROR('message')
CLOSE
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.
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.
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
(* /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
/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.
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